modelhealth

Model Health Python SDK.

Biomechanical analysis from smartphone videos.

Example:
from modelhealth import ModelHealthClient, SubjectParameters

client = ModelHealthClient("your_api_key")

sessions = client.session_list()
subjects = client.subjects.list().all()
class ModelHealthClient:

Client for the Model Health biomechanical analysis platform.

Enables you to measure and analyze human movement from smartphone videos. Provides a complete workflow for:

  1. Initialization: Create a client with your API key
  2. Session Creation: Create a session
  3. Camera Calibration: Calibrate cameras using a checkerboard pattern
  4. Subject Creation: Create a subject profile with anthropometric measurements
  5. Subject Calibration: Calibrate subject using videos of a neutral pose
  6. Movement Recording: Record movement activities (squats, jumps, etc.)
  7. Movement Analysis: Trigger advanced analysis and fetch processed biomechanical data

All methods are synchronous and block until the underlying operation completes. Each instance owns a dedicated single-threaded async runtime so instances may be used across threads, but a single instance should not be called concurrently from multiple threads.

Attributes:
Example:
from modelhealth import (
    ModelHealthClient, SubjectParameters, CheckerboardDetails,
    CheckerboardPlacement, ActivityType,
)

client = ModelHealthClient("your_api_key")

# Create session and calibrate cameras
session = client.create_session()
details = CheckerboardDetails(rows=4, columns=5, square_size=35, placement=CheckerboardPlacement.perpendicular)
client.calibrate_camera(session, details, lambda s: print(s))

# Create subject and calibrate
params = SubjectParameters(name="Jane Doe", weight=65.0, height=170.0, birth_year=1990)
subject = client.create_subject(params)
client.calibrate_subject(subject, session, lambda s: print(s))

# Record a movement activity
activity = client.start_recording("cmj", session)
# Subject performs movement...
client.stop_recording(session)
ModelHealthClient( api_key: str, timeout: float | None = None, max_retries: int | None = None)

Initializes the client.

Arguments:
  • api_key: Your Model Health API key, available in the dashboard at modelhealth.io.
  • timeout: Per-request timeout in seconds for JSON API calls. Bulk transfers (archive downloads, video uploads) are not bounded by it. If omitted, the build's default is used.
  • max_retries: Number of retries on transient network or request failures. If omitted, the build's default is used.
Raises:
  • AuthenticationError: If the API key is not valid.
Example:
from modelhealth import ModelHealthClient

client = ModelHealthClient("your_api_key")

# List sessions and subjects
sessions = client.session_list()
subjects = client.subjects.list().all()
activities
subjects
sessions
groups
activity_types
def account_info(self) -> AccountInfo:

Verifies the API key and returns information about the authenticated account.

A cheap way to check that the API key is valid without performing a domain operation.

Returns:

An AccountInfo with the authenticated account's identity and licensing details.

Raises:
  • AuthenticationError: If the API key is invalid or expired.
  • NetworkError: On connection failure or timeout.
Example:
info = client.account_info()
print(f"Authenticated as {info.email}")
def usage(self) -> UsageInfo:

Returns the current billing/quota state for the authenticated account.

Use this to show usage bars and plan limits, and to block recording in your own UI when the quota is exhausted or the subscription has lapsed.

reason explains why recording_allowed is False, and is only set in that case. Most other detail fields (reason, activities_used, activities_max, period_end, plan_name, reset_period) are None when there is no active plan. An activities_max of None while a plan is active means the plan is unlimited.

Returns:

A UsageInfo describing the account's current billing and quota state.

Raises:
  • AuthenticationError: If the API key is invalid or expired.
  • NetworkError: On connection failure or timeout.
Example:
usage = client.usage()
if not usage.recording_allowed:
    print(f"Recording blocked: {usage.reason}")
def session_list(self) -> list[Session]:

Retrieves all sessions for the account associated with the API key.

Use this to list existing sessions before creating a new one, or to resume a previous capture workflow.

To connect a device to a specific session, use the session qrcode URL to download the QR code image data, then display it in your app to be captured by the ModelHealth mobile app.

Returns:

A list of Session objects, or an empty list if none exist.

Raises:
  • AuthenticationError: If the API key is invalid or expired.
  • NetworkError: On connection failure or timeout.
  • ModelHealthError: On request failure or an unexpected response.
Example:
import urllib.request

sessions = client.session_list()
print(f"Found {len(sessions)} sessions")

if sessions:
    first = sessions[0]
    with urllib.request.urlopen(first.qrcode) as resp:
        qr_image_data = resp.read()
    # Display qr_image_data in your app so the mobile app can scan it
def get_session(self, session_id: str) -> Session:

Retrieve a specific session by ID with all activities populated.

Arguments:
  • session_id: The id of the session to retrieve.
Returns:

A Session with activities populated.

Raises:
  • AuthenticationError: If the API key is invalid or expired.
  • NotFoundError: If the session does not exist.
  • NetworkError: On connection failure or timeout.
  • ModelHealthError: On request failure or an unexpected response.
Example:
session = client.get_session("abc123")
for activity in session.activities:
    print(f"{activity.name or activity.id}")
def switch_subject(self, subject: Subject, session: Session) -> Session:

Points the cameras at the session to record subject in.

Use this to move between subjects in any order, including going back to someone recorded earlier. The session already recording that subject is reused when its camera calibration and static pose are both still good, so the pose does not have to be repeated. Otherwise a new session is started from session, exactly as new_session_from_session() would.

Arguments:
  • subject: The subject to record next.
  • session: The session the cameras are pointed at now.
Returns:

The Session to record in. May be one that already existed, the one passed in, or a new one.

Raises:
  • AuthenticationError: If the API key is invalid or expired.
  • NotFoundError: If the session or subject does not exist.
  • UnsupportedOperationError: If the API version in use cannot hand the cameras back to an existing session.
  • NetworkError: On connection failure or timeout.
  • ModelHealthError: On request failure or an unexpected response.
Example:
# Everyone does exercise 1, then everyone does exercise 2
for exercise in ("CMJ", "Squat"):
    for subject in subjects:
        session = client.switch_subject(subject, session)
        activity = client.start_recording(exercise, session)
        client.stop_recording(session)
def new_session_from_session(self, session: Session) -> Session:

Creates a new session from a previous session, inheriting its calibration setup.

Use this to move directly to calibrating a new subject without repeating camera connection or checkerboard calibration.

Arguments:
  • session: The previous, already-calibrated session.
Returns:

A new Session ready for a new subject.

Raises:
  • AuthenticationError: If the API key is invalid or expired.
  • NotFoundError: If the session does not exist.
  • NetworkError: On connection failure or timeout.
  • ModelHealthError: On request failure or an unexpected response.
Example:
new_session = client.new_session_from_session(previous_session)
def create_session(self) -> Session:

Creates a session.

A session is the parent container for a movement capture workflow. It links related entities such as activities and subjects and provides the context used by subsequent operations.

Default session settings are applied automatically. Call configure_session() afterwards to override specific settings.

Returns:

A new Session with a unique identifier.

Raises:
  • AuthenticationError: If the API key is invalid or expired.
  • NetworkError: On connection failure or timeout.
  • ModelHealthError: On request failure or an unexpected response.
Example:
session = client.create_session()
def configure_session( self, session: Session, framerate: SessionFramerate = <SessionFramerate.fps_120: 'fps_120'>, opensim_model: SessionOpenSimModel = <SessionOpenSimModel.lai_uhlrich_2022: 'lai_uhlrich_2022'>, scaling_setup: SessionScalingSetup = <SessionScalingSetup.upright_standing_pose: 'upright_standing_pose'>, core_engine: SessionCoreEngine = <SessionCoreEngine.v1_0: 'v1_0'>, filter_frequency: Union[FilterFrequencyDefault, FilterFrequencyHz] = <builtins.FilterFrequencyDefault object>, data_sharing: SessionDataSharing = <SessionDataSharing.share_processed_data_and_identified_videos: 'share_processed_data_and_identified_videos'>) -> None:

Applies settings to an existing session.

Call this after create_session() and before calibration to override any default settings. If not called, the session retains the defaults applied during creation.

All parameters are optional — omit any to keep its default.

Arguments:
Raises:
  • AuthenticationError: If the API key is invalid or expired.
  • NetworkError: On connection failure or timeout.
  • ModelHealthError: On request failure or an unexpected response.
Example:
session = client.create_session()
client.configure_session(
    session,
    framerate=SessionFramerate.fps_60,
    data_sharing=SessionDataSharing.share_no_data,
)
def fetch_subject(self, subject_id: int) -> Subject:

Retrieves a subject by its ID.

Use this to resolve a subject ID (e.g. from Session.subject) into full subject details without fetching the entire subject list.

Arguments:
  • subject_id: The unique identifier of the subject.
Returns:

The Subject with its current details.

Raises:
  • AuthenticationError: If the API key is invalid or expired.
  • NotFoundError: If the subject does not exist.
  • NetworkError: On connection failure or timeout.
  • ModelHealthError: On request failure or an unexpected response.
Example:
subject = client.fetch_subject(session.subject)
print(f"Subject: {subject.name}")
def create_subject(self, parameters: SubjectParameters) -> Subject:

Creates a subject profile.

Height and weight are required for biomechanical analysis. Once created, the subject can be calibrated using calibrate_subject().

Arguments:
  • parameters: The subject's profile details including name and anthropometrics.
Returns:

The newly created Subject with its assigned ID.

Raises:
  • AuthenticationError: If the API key is invalid or expired.
  • NetworkError: On connection failure or timeout.
  • ModelHealthError: On validation failure, request failure or an unexpected response.
Example:
from modelhealth import SubjectParameters

params = SubjectParameters(
    name="John Smith",
    weight=75.0,
    height=180.0,
    birth_year=1990,
)
subject = client.create_subject(params)
print(f"Created subject with ID: {subject.id}")

# Use the subject for calibration
client.calibrate_subject(subject, session, lambda s: print(s))
def fetch_activity(self, activity_id: str) -> Activity:

Retrieves an activity by its ID.

Use this to fetch the latest state of an activity, including its videos, results and current processing status.

Arguments:
  • activity_id: The unique identifier of the activity.
Returns:

The Activity with its current details.

Raises:
  • AuthenticationError: If the API key is invalid or expired.
  • NotFoundError: If the activity does not exist.
  • NetworkError: On connection failure or timeout.
  • ModelHealthError: On request failure or an unexpected response.
Example:
activity = client.fetch_activity("abc123")
print(f"Activity: {activity.name or 'Unnamed'}")
print(f"Status: {activity.status}")
def update_activity( self, activity: Activity, config: ActivityConfig | None = None) -> Activity:

Updates an activity.

Only mutable fields (such as name) are applied. Use the returned object rather than the input going forward.

Arguments:
  • activity: The Activity to update, with modified properties.
  • config: Optional config to apply alongside the update. Supports add_tags/remove_tags to modify tags and name to rename.
Returns:

The updated Activity as stored.

Raises:
  • AuthenticationError: If the API key is invalid or expired.
  • NotFoundError: If the activity does not exist.
  • NetworkError: On connection failure or timeout.
  • ModelHealthError: On request failure or an unexpected response.
def delete_activity(self, activity: Activity) -> None:

Deletes an activity.

Permanently removes the activity and all associated videos, results, and metadata.

Arguments:
Raises:
  • AuthenticationError: If the API key is invalid or expired.
  • NotFoundError: If the activity does not exist.
  • NetworkError: On connection failure or timeout.
  • ModelHealthError: On request failure or an unexpected response.
Example:
client.delete_activity(activity)
Warning:

This operation is irreversible.

def activity_tags(self) -> list[ActivityTag]:

Retrieves all available activity tags.

Use the returned tags to populate a tag picker or validate tag values before assigning them to activities.

Returns:

A list of ActivityTag objects, or an empty list if none are configured.

Raises:
  • AuthenticationError: If the API key is invalid or expired.
  • NetworkError: On connection failure or timeout.
  • ModelHealthError: On request failure or an unexpected response.
Example:
tags = client.activity_tags()
for tag in tags:
    print(f"{tag.label}: {tag.value}")
def activity_list(self, session: Session) -> list[Activity]:

Retrieves all movement activities associated with a session.

Activities represent individual recording trials and contain references to captured videos and results. Use this to review past data or fetch results for completed activities.

Arguments:
  • session: The session whose activities to retrieve.
Returns:

A list of Activity objects, or an empty list if none exist.

Raises:
  • AuthenticationError: If the API key is invalid or expired.
  • NetworkError: On connection failure or timeout.
  • ModelHealthError: On request failure or an unexpected response.
Example:
activities = client.activity_list(session)
for activity in activities:
    print(f"{activity.name or activity.id}")
    print(f"  Videos: {len(activity.videos)}")
    print(f"  Results: {len(activity.results)}")
def add_motion_data_to_activity(self, activity: Activity, files: list[ExternalResultFile]) -> Activity:

Uploads external files (e.g. sensor data) to an activity.

Posts each file as a separate result entry and returns a refreshed activity reflecting the new results. All files are uploaded in parallel.

Arguments:
Returns:

A refreshed Activity containing the new result entries.

Raises:
  • ModelHealthError: If any tag is reserved, empty or duplicated, or if any file exceeds the 100 MB size limit.
  • AuthenticationError: If the API key is invalid or expired.
  • NetworkError: On connection failure or timeout.
  • ModelHealthError: On request failure or an unexpected response.
Example:
from modelhealth import ExternalResultFile

with open("sensor_data.csv", "rb") as f:
    csv_data = f.read()

file = ExternalResultFile("acme-sensor", "csv", csv_data)
activity = client.add_motion_data_to_activity(activity, [file])
def videos_for_activity( self, activity: Activity, version: VideoVersion) -> list[bytes]:

Downloads video data for a specific activity.

Fetches all videos associated with the activity that match the specified version. Videos with invalid URLs or failed downloads are silently excluded from the result.

Arguments:
Returns:

A list of raw video bytes (one per camera). Write each element to a .mp4 file for playback. May be empty if no videos are available or all downloads fail.

Raises:
  • AuthenticationError: If the API key is invalid or expired.
  • NetworkError: On connection failure or timeout.
  • ModelHealthError: On request failure or an unexpected response.
Example:
videos = client.videos_for_activity(activity, VideoVersion.synced)
for i, video in enumerate(videos):
    with open(f"video_{i}.mp4", "wb") as f:
        f.write(video)
def motion_data_for_activity( self, activity: Activity, data_types: list[MotionDataType]) -> list[MotionData]:

Downloads motion data from a processed activity.

Use this after an activity reaches ActivityStatus.ready to retrieve biomechanical result files such as kinematics, marker data, or an OpenSim model. Failed downloads are silently excluded from results.

Arguments:
  • activity: The Activity whose results to download. Must have completed processing.
  • data_types: Motion data types to fetch. Valid values: "animation", "kinematics_mot", "kinematics_csv", "markers_trc", "markers_csv", "model".
Returns:

A list of MotionData objects, one per successfully downloaded type. May be empty if no results are available or all downloads fail.

Raises:
  • AuthenticationError: If the API key is invalid or expired.
  • NetworkError: On connection failure or timeout.
  • ModelHealthError: On request failure or an unexpected response.
Example:
results = client.motion_data_for_activity(
    activity, [MotionDataType.kinematics_mot, MotionDataType.animation]
)
for r in results:
    if r.type == "kinematics_mot":
        with open("kinematics.mot", "wb") as f:
            f.write(r.data)  # MOT format
    elif r.type == "animation":
        import json
        transforms = json.loads(r.data)  # JSON format
    elif r.type == "markers_trc":
        with open("markers.trc", "wb") as f:
            f.write(r.data)  # TRC format
    elif r.type == "model":
        with open("model.osim", "wb") as f:
            f.write(r.data)  # OSim format
def analysis_data_for_activity( self, activity: Activity, data_types: list[AnalysisDataType]) -> list[AnalysisData]:

Downloads result data for an activity with a completed analysis.

Call this after analysis_status() returns AnalysisStatus.completed to retrieve metrics, a report, or raw data. Failed downloads are silently excluded from results.

Arguments:
  • activity: The Activity whose analysis results to download. Must have a completed analysis.
  • data_types: Analysis result types to fetch. Valid values: "metrics" (JSON), "report" (PDF), "data" (ZIP).
Returns:

A list of AnalysisData objects, one per successfully downloaded type. May be empty if no results are available or all downloads fail.

Raises:
  • AuthenticationError: If the API key is invalid or expired.
  • NetworkError: On connection failure or timeout.
  • ModelHealthError: On request failure or an unexpected response.
Example:
import json

results = client.analysis_data_for_activity(
    activity, [AnalysisDataType.metrics, AnalysisDataType.report, AnalysisDataType.data]
)
for r in results:
    if r.type == "metrics":
        metrics = json.loads(r.data)
    elif r.type == "report":
        with open("report.pdf", "wb") as f:
            f.write(r.data)
    elif r.type == "data":
        with open("data.zip", "wb") as f:
            f.write(r.data)
def activity_status( self, activity: Activity) -> Union[ActivityStatus, ActivityStatusUploading, ActivityStatusAnalyzing]:

Retrieves the current processing status of an activity.

Poll this method after recording stops to track upload and processing progress.

Returns ActivityStatusUploading when videos are uploading, ActivityStatusAnalyzing (with the analysis task) when automatic analysis is running, or an ActivityStatus enum value otherwise.

Arguments:
  • activity: The Activity to check status for.
Returns:

The current status as ActivityStatusUploading, ActivityStatusAnalyzing, or ActivityStatus.

Raises:
  • AuthenticationError: If the API key is invalid or expired.
  • NetworkError: On connection failure or timeout.
  • ModelHealthError: On request failure or an unexpected response.
Example:
status = client.activity_status(activity)
if isinstance(status, ActivityStatusUploading):
    print(f"Uploading: {status.uploaded}/{status.total}")
elif status == ActivityStatus.processing:
    print("Still processing...")
elif isinstance(status, ActivityStatusAnalyzing):
    analysis_status = client.analysis_status(status.task)
elif status == ActivityStatus.ready:
    print("Activity ready for analysis")
elif status == ActivityStatus.failed:
    print("Processing failed")
def start_analysis( self, activity_type: ActivityType, activity: Activity, session: Session) -> Analysis:

Starts an analysis task for an activity that is ready for analysis.

Call this after activity_status() returns ActivityStatus.ready. Use the returned Analysis with analysis_status() to poll progress.

Arguments:
  • activity_type: The analysis algorithm to run. Use a ActivityType member.
  • activity: The Activity to analyze.
  • session: The Session containing the activity.
Returns:

An Analysis for tracking analysis progress.

Raises:
  • AuthenticationError: If the API key is invalid or expired.
  • NetworkError: On connection failure or timeout.
  • ModelHealthError: If the activity is not ready or the request fails.
Example:
task = client.start_analysis(
    ActivityType.counter_movement_jump, activity, session
)
status = client.analysis_status(task)
def analysis_status(self, task: Analysis) -> AnalysisStatus:

Retrieves the current status of an analysis task.

Poll this method after start_analysis() to monitor progress. When status reaches AnalysisStatus.completed, download results with analysis_data_for_activity().

Arguments:
Returns:

The current AnalysisStatus.

Raises:
  • AuthenticationError: If the API key is invalid or expired.
  • NetworkError: On connection failure or timeout.
  • ModelHealthError: On request failure or an unexpected response.
Example:
import time

status = client.analysis_status(task)
while status == AnalysisStatus.processing:
    time.sleep(10)
    status = client.analysis_status(task)

if status == AnalysisStatus.completed:
    results = client.analysis_data_for_activity(
        activity, [AnalysisDataType.metrics, AnalysisDataType.report]
    )
elif status == AnalysisStatus.failed:
    print("Analysis failed")
def start_recording( self, name: str, session: Session, config: ActivityConfig | None = None) -> Activity:

Creates an activity and starts recording a dynamic movement trial.

Must be called after both camera and subject calibration are complete. Call stop_recording() when the subject has finished the movement.

Arguments:
  • name: A descriptive name for this activity (e.g. "cmj", "squat").
  • session: The session this activity is associated with.
  • config: Optional recording configuration.
Returns:

The newly created Activity.

Raises:
  • AuthenticationError: If the API key is invalid or expired.
  • NetworkError: On connection failure or timeout.
  • ModelHealthError: If recording cannot start (e.g. missing calibration).
Example:
activity = client.start_recording("cmj", session, ActivityConfig(ActivityType.counter_movement_jump))
# Subject performs movement...
client.stop_recording(session)
def stop_recording(self, session: Session) -> None:

Stops the active recording for a movement trial.

Call this after the subject has completed the movement. Once stopped, the recorded videos begin uploading and can be tracked with activity_status().

Arguments:
  • session: The session context to stop recording in.
Raises:
  • AuthenticationError: If the API key is invalid or expired.
  • NetworkError: On connection failure or timeout.
  • ModelHealthError: If there is no active recording or the request fails.
Example:
client.stop_recording(session)
def calibrate_camera( self, session: Session, checkerboard: CheckerboardDetails, status_callback: object) -> None:

Calibrates cameras using a checkerboard pattern.

Determines each camera's position and orientation in 3D space (extrinsics), enabling reconstruction of real-world movement from multiple 2D video feeds. Required once per session setup — recalibrate only if cameras are moved.

Note:

rows and columns refer to internal corners, not squares. A 5×6 board has 4 internal corner rows and 5 internal corner columns.

This method blocks until calibration completes. The status_callback is called synchronously on each status update during the operation.

Arguments:
Raises:
  • AuthenticationError: If the API key is invalid or expired.
  • NetworkError: On connection failure or timeout.
  • ModelHealthError: If calibration fails (e.g. pattern not detected).
Example:
from modelhealth import CheckerboardDetails, CheckerboardPlacement

details = CheckerboardDetails(
    rows=4,       # Internal corners, not squares (for 5×6 board)
    columns=5,    # Internal corners, not squares (for 5×6 board)
    square_size=35,  # Measured in millimeters
    placement=CheckerboardPlacement.perpendicular,
)
client.calibrate_camera(session, details, lambda s: print(s))
def calibrate_subject( self, subject: Subject, session: Session, status_callback: object) -> None:

Calibrates a subject by recording a neutral standing pose.

Scales the 3D biomechanical model to the subject's body size. Must be run after camera calibration and requires the subject profile to include height and weight.

Important:

The subject must stand upright, feet pointing forward, completely still and fully visible to all cameras for the duration of the recording.

This method blocks until calibration completes. The status_callback is called synchronously on each status update during the operation.

Arguments:
Raises:
  • AuthenticationError: If the API key is invalid or expired.
  • NetworkError: On connection failure or timeout.
  • ModelHealthError: If calibration fails.
Example:
client.calibrate_subject(subject, session, lambda s: print(s))
def prepare_archive(self, session: Session, with_videos: bool = False) -> Archive:

Starts preparing a session archive for download.

Packaging the session data into a ZIP file starts in the background. Poll archive_status() until status reaches ArchiveStatus.ready, then call archive_data() to download the ZIP file.

Arguments:
  • session: The Session to archive.
  • with_videos: If True, include raw video files in the archive. Defaults to False.
Returns:

An Archive for tracking preparation progress.

Raises:
  • AuthenticationError: If the API key is invalid or expired.
  • NetworkError: On connection failure or timeout.
  • ModelHealthError: On request failure or an unexpected response.
Example:
import time

archive = client.prepare_archive(session)
status = client.archive_status(archive)
while status == ArchiveStatus.processing:
    time.sleep(5)
    status = client.archive_status(archive)

if status == ArchiveStatus.ready:
    data = client.archive_data(archive)
    with open("session.zip", "wb") as f:
        f.write(data)
def archive_status(self, archive: Archive) -> ArchiveStatus:

Retrieves the current preparation status of an archive task.

Poll this method after prepare_archive() to monitor progress. When status reaches ArchiveStatus.ready, download the archive with archive_data().

Arguments:
Returns:

The current ArchiveStatus.

Raises:
  • AuthenticationError: If the API key is invalid or expired.
  • NetworkError: On connection failure or timeout.
  • ModelHealthError: On request failure or an unexpected response.
Example:
status = client.archive_status(archive)

if status == ArchiveStatus.processing:
    print("Archive being prepared...")
elif status == ArchiveStatus.ready:
    print("Archive ready to download")
elif status == ArchiveStatus.failed:
    print("Archive preparation failed")
def import_session( self, activities_json: str, subject: Subject, session_config: SessionConfig | None = None, session_meta_json: str | None = None, status_callback=None) -> Session:

Imports a set of activities into Model Health.

Performs the full import workflow: session creation, configuration, subject association, activity creation, video download and upload, processing trigger and polling until complete for each activity.

Arguments:
  • activities_json: Raw JSON string of a JSON array of activity objects.
  • subject: Subject to associate with the session.
  • session_config: Session settings. None uses defaults.
  • session_meta_json: Optional JSON string of the source session's meta field. When provided, the new session is patched with this metadata before settings are applied, preserving source data (e.g. sessionWithCalibration) needed for processing.
  • status_callback: Optional callable invoked at each step with a status value. Receives one of: "creating_session", ImportStatusCreatedSession, ImportStatusUploading, or "processing".
Returns:

The Session containing all imported activities.

Raises:
  • AuthenticationError: If the API key is invalid or expired.
  • NetworkError: On connection failure or timeout.
  • ModelHealthError: On request failure, invalid JSON or processing failure.
Example:
import json

activities_json = json.dumps([{...}, {...}])  # list of activity dicts

def on_status(status):
    if isinstance(status, ImportStatusUploading):
        print(f"[{status.trial}] Uploading {status.uploaded}/{status.total}")
    else:
        print(status)

session = client.import_session(
    activities_json,
    subject,
    status_callback=on_status,
)
def archive_data(self, archive: Archive) -> bytes:

Downloads the prepared session archive as a ZIP file.

Call this after archive_status() returns ArchiveStatus.ready.

Arguments:
Returns:

Raw ZIP bytes. Write directly to a .zip file.

Raises:
  • AuthenticationError: If the API key is invalid or expired.
  • NetworkError: On connection failure or timeout.
  • ModelHealthError: If the archive is not ready or the download fails.
Example:
data = client.archive_data(archive)
with open("session.zip", "wb") as f:
    f.write(data)
def activity_metrics(self, activity_id: str) -> ActivityMetrics | None:

Returns metric values for a single activity.

Fetches the full set of biomechanical metric values computed for the given activity, grouped by category.

Arguments:
  • activity_id: UUID of the activity.
Returns:

An ActivityMetrics instance containing all metric groups and their computed values, or None if the activity has not been analysed yet.

Raises:
  • AuthenticationError: If the API key is invalid or expired.
  • NotFoundError: If no activity with activity_id exists.
  • NetworkError: On connection failure or timeout.
  • ModelHealthError: On request failure or an unexpected response.
Example:
metrics = client.activity_metrics("a1b2c3d4-...")
if metrics is None:
    print("No metrics yet — this activity has not been analysed.")
else:
    for group in metrics.groups:
        print(group.name)
        for m in group.metrics:
            print(f"  {m.name}: {m.value}")
def subject_metrics( self, subject_id: int, start: str | None = None, end: str | None = None) -> list[ActivityMetrics]:

Returns metric values for all of a subject's activities.

Fetches metric values across every activity belonging to the subject, optionally filtered to a date range.

Arguments:
  • subject_id: Numeric ID of the subject.
  • start: Optional ISO date string (YYYY-MM-DD) for the start of the date range (inclusive).
  • end: Optional ISO date string (YYYY-MM-DD) for the end of the date range (inclusive).
Returns:

A list of ActivityMetrics instances, one per activity.

Raises:
  • AuthenticationError: If the API key is invalid or expired.
  • NotFoundError: If no subject with subject_id exists.
  • NetworkError: On connection failure or timeout.
  • ModelHealthError: On request failure or an unexpected response.
Example:
all_metrics = client.subject_metrics(42, start="2025-01-01", end="2025-06-30")
for activity_metrics in all_metrics:
    print(activity_metrics.activity_id)
def set_video_upload_mode(self, mode: VideoUploadMode) -> None:

Sets the video upload mode

Arguments:
  • mode: The desired video upload mode.
Raises:
  • AuthenticationError: If the API key is invalid or expired.
  • NetworkError: On connection failure or timeout.
  • ModelHealthError: On request failure or an unexpected response.
Example:
client.set_video_upload_mode(VideoUploadMode.disabled)
def set_log_level(self, level: LogLevel) -> None:

Adjusts the log level without touching the registered handler.

Arguments:
  • level: How verbose the log event stream should be.
Example:
client.set_log_level(LogLevel.warn)
def set_log_handler( self, handler: Optional[Callable[[LogEvent], NoneType]], level: LogLevel = <LogLevel.info: 'info'>) -> None:

Registers or clears the persistent log handler.

Passing None for handler clears it regardless of level.

Arguments:
  • handler: Called with a LogEvent for each event at or below level. Pass None to stop receiving events.
  • level: How verbose the log event stream should be. Ignored when handler is None.
Example:
def handle_log(event: LogEvent) -> None:
    print(f"[modelhealth] {event.code}: {event.message}")

client.set_log_handler(handle_log)

# Stop receiving events
client.set_log_handler(None)
class ModelHealthService(ModelHealthClient):

Deprecated alias for ModelHealthClient.

Deprecated since version : Use ModelHealthClient instead. Kept for backward compatibility; will be removed no sooner than two minor releases and six months after this deprecation.

ModelHealthService(api_key: str)

Initializes the client.

Arguments:
  • api_key: Your Model Health API key, available in the dashboard at modelhealth.io.
  • timeout: Per-request timeout in seconds for JSON API calls. Bulk transfers (archive downloads, video uploads) are not bounded by it. If omitted, the build's default is used.
  • max_retries: Number of retries on transient network or request failures. If omitted, the build's default is used.
Raises:
  • AuthenticationError: If the API key is not valid.
Example:
from modelhealth import ModelHealthClient

client = ModelHealthClient("your_api_key")

# List sessions and subjects
sessions = client.session_list()
subjects = client.subjects.list().all()
class ActivitiesResource(_BaseResource):

Filtered access to activities.

Returned by ModelHealthClient.activities — do not construct directly.

def list( self, subject: Subject | int | None = None, calibration_session: Session | str | None = None, activity_type: ActivityTypeInfo | int | None = None, exclude_calibration: bool = True, created_after: datetime.date | str | None = None, created_before: datetime.date | str | None = None, search: str | None = None, tags: "'list[str] | None'" = None, created_by: "'list[int] | None'" = None, only_completed: bool = False, exclude_analysis_error: bool = False, order_by: str | None = None, limit: int | None = None) -> ActivityStream:

Lazily lists activities matching the given filters.

Arguments:
  • subject: Only activities belonging to this subject. Accepts a Subject or its id.
  • calibration_session: Only activities recorded under this calibration session. Accepts a Session or its id.
  • activity_type: Only activities of this type.
  • exclude_calibration: Excludes calibration and neutral-pose activities. Default True, since these are rarely what you want when browsing a subject's movement history.
  • created_after: Only activities created on or after this date (inclusive). Accepts a datetime.date or an ISO YYYY-MM-DD string. A datetime.datetime counts as the day it falls on in its own zone, or in the local one if it carries none.
  • created_before: Only activities created on or before this date (inclusive). As created_after.
  • search: Free-text search against the activity's name.
  • tags: Only activities carrying every tag in this list.
  • created_by: Only activities belonging to one of these accounts, by numeric account id.
  • only_completed: Only activities whose analysis has finished.
  • exclude_analysis_error: Excludes activities whose analysis failed.
  • order_by: Field to sort by — one of "created_at", "status", "created_by", "activity_type" — optionally prefixed with "-" for descending. Defaults to "-created_at".
  • limit: Caps the total number of activities the stream yields across the whole iteration. Unset iterates every match.
Returns:

An ActivityStream you iterate directly.

Raises:
  • AuthenticationError: If the API key is invalid or expired.
  • NetworkError: On connection failure or timeout.
  • ModelHealthError: If order_by or activity_type isn't recognized, or on request failure or an unexpected response.
Example:
activities = client.activities.list(
    subject=subject,
    activity_type=ActivityType.squats,
    created_after=date(2026, 1, 1),
    order_by="-created_at",
)
for activity in activities:
    print(activity.name)

total = client.activities.list(tags=["study-a"]).total
all_activities = client.activities.list(tags=["study-a"]).all()
Inherited Members
_BaseResource
_BaseResource
class ActivityStream(modelhealth._ItemStream[Activity]):

An iterable sequence of activities matching a filter.

Returned by ActivitiesResource.list(). Read total for the number of activities matching the filter, or call all() to collect what is left.

Inherited Members
_ItemStream
_ItemStream
total
all
class SubjectsResource(_BaseResource):

Filtered access to subjects.

Returned by ModelHealthClient.subjects — do not construct directly.

def list( self, search: str | None = None, created_after: datetime.date | str | None = None, created_before: datetime.date | str | None = None, groups: "'list[str] | None'" = None, tags: "'list[str] | None'" = None, created_by: "'list[int] | None'" = None, activity_type: ActivityTypeInfo | int | None = None, activity_complete: bool | None = None, session: Session | str | None = None, order_by: str | None = None, limit: int | None = None) -> SubjectStream:

Lazily lists subjects matching the given filters.

Arguments:
  • search: Free-text search against the subject's name.
  • created_after: Only subjects created on or after this date (inclusive). Accepts a datetime.date or an ISO YYYY-MM-DD string. A datetime.datetime counts as the day it falls on in its own zone, or in the local one if it carries none.
  • created_before: Only subjects created on or before this date (inclusive). As created_after.
  • groups: Only subjects belonging to one of these subject groups.
  • tags: Only subjects carrying every tag in this list.
  • created_by: Only subjects belonging to one of these accounts, by numeric account id.
  • activity_type: Only subjects with at least one activity of this type.
  • activity_complete: Only subjects whose activities have (True) or have not (False) all finished analysis.
  • session: Only the subject calibrated under this session. Accepts a Session or its id.
  • order_by: Field to sort by — one of "name", "created_at", "updated_at" — optionally prefixed with "-" for descending. Defaults to "name".
  • limit: Caps the total number of subjects the stream yields across the whole iteration. Unset iterates every match.
Returns:

A SubjectStream you iterate directly.

Raises:
  • AuthenticationError: If the API key is invalid or expired.
  • NetworkError: On connection failure or timeout.
  • ModelHealthError: If order_by or activity_type isn't recognized, or on request failure or an unexpected response.
Example:
subjects = client.subjects.list(search="Falisse", tags=["study-a"])
for subject in subjects:
    print(subject.name)

total = client.subjects.list(tags=["study-a"]).total
all_subjects = client.subjects.list(tags=["study-a"]).all()
Inherited Members
_BaseResource
_BaseResource
class SubjectStream(modelhealth._ItemStream[Subject]):

An iterable sequence of subjects matching a filter.

Returned by SubjectsResource.list(). Read total for the number of subjects matching the filter, or call all() to collect what is left.

Inherited Members
_ItemStream
_ItemStream
total
all
class SessionsResource(_BaseResource):

Filtered access to sessions.

Returned by ModelHealthClient.sessions — do not construct directly.

def list( self, subject: Subject | int | None = None, order_by: str | None = None, limit: int | None = None) -> SessionStream:

Lazily lists sessions matching the given filters.

Arguments:
  • subject: Only the session(s) belonging to this subject. Accepts a Subject or its id.
  • order_by: Field to sort by — one of "name", "created_at" — optionally prefixed with "-" for descending. Defaults to "-created_at".
  • limit: Caps the total number of sessions the stream yields across the whole iteration. Unset iterates every match.
Returns:

A SessionStream you iterate directly.

Raises:
  • AuthenticationError: If the API key is invalid or expired.
  • NetworkError: On connection failure or timeout.
  • ModelHealthError: If order_by isn't recognized, or on request failure or an unexpected response.
Example:
for session in client.sessions.list(subject=subject):
    print(session.id)

total = client.sessions.list(subject=subject).total
Inherited Members
_BaseResource
_BaseResource
class SessionStream(modelhealth._ItemStream[Session]):

An iterable sequence of sessions matching a filter.

Returned by SessionsResource.list(). Read total for the number of sessions matching the filter, or call all() to collect what is left.

Inherited Members
_ItemStream
_ItemStream
total
all
class GroupsResource(_BaseResource):

Filtered access to subject groups.

Returned by ModelHealthClient.groups — do not construct directly.

def list( self, search: str | None = None, order_by: str | None = None, limit: int | None = None) -> GroupStream:

Lazily lists subject groups matching the given filters.

Arguments:
  • search: Free-text search against the group's name.
  • order_by: Field to sort by — currently only "name" — optionally prefixed with "-" for descending. Defaults to "name".
  • limit: Caps the total number of groups the stream yields across the whole iteration. Unset iterates every match.
Returns:

A GroupStream you iterate directly.

Raises:
  • AuthenticationError: If the API key is invalid or expired.
  • NetworkError: On connection failure or timeout.
  • ModelHealthError: If order_by isn't recognized, or on request failure or an unexpected response.
Example:
for group in client.groups.list(search="cohort"):
    print(group.name)

total = client.groups.list().total
Inherited Members
_BaseResource
_BaseResource

An iterable sequence of subject groups matching a filter.

Returned by GroupsResource.list(). Read total for the number of subject groups matching the filter, or call all() to collect what is left.

Inherited Members
_ItemStream
_ItemStream
total
all
class SubjectGroup:

A named collection of subjects.

trashed_at

When the group was trashed, or None if it isn't.

subject_count

Number of subjects in this group.

trashed

Whether the group is in the trash.

name

Display name.

created_by

Username of the account that created this group, or None if not reported.

total_activities

Total activities recorded across every subject in this group.

description

Freeform description, or an empty string if none was set.

can_edit

Whether the authenticated account can edit this group.

id

Unique identifier.

created_at

When the group was created.

is_owner

Whether the authenticated account owns this group.

last_activity

Timestamp of the most recent activity across the group, or None if there are none.

updated_at

When the group was last modified.

class ModelHealthError(builtins.RuntimeError):

Base exception for all Model Health SDK errors.

class AuthenticationError(ModelHealthError):

Raised when the API key is missing, invalid or expired.

class NotFoundError(ModelHealthError):

Raised when a requested resource does not exist.

class NetworkError(ModelHealthError):

Raised on connection failure, timeout or DNS error.

class UnsupportedOperationError(ModelHealthError):

Raised when the operation is not supported by this client configuration.

class AccountInfo:

Identity information for the authenticated account.

Returned by ModelHealthClient.account_info().

profession
username
last_name
email
institution
country
first_name
class UsageInfo:

Current billing/quota state for the authenticated account.

Returned by ModelHealthClient.usage().

plan_name

Name of the current plan, or None when there is no active plan.

is_free_trial

Whether the account is on a free trial, or None when there is no organization, or no plan history at all.

recording_allowed

Whether recording is currently allowed.

reason

Why recording is blocked, or None when recording_allowed is True.

activities_max

Activity cap for the current period, or None when there is no active plan, or the plan is unlimited.

activities_used

Activities consumed in the current period, or None when there is no active plan.

reset_period

How often activities_used resets, or None when there is no active plan.

will_auto_renew

Whether the plan will renew automatically at period_end, or None when there is no active plan or no subscription to renew.

period_end

When the current billing period ends, or None when there is no active plan.

class Session:

A parent container for a movement capture workflow.

updated_at

When this session was last updated.

subject

ID of the subject associated with this session, or None if not set.

A bare ID, not a Subject object.

activities

Activities associated with this session.

user

ID of the account that owns this session.

created_at

When this session was created.

name

Display name of the session.

session_name

Internal session name. Prefer name for display purposes.

activities_count

Total number of activities in this session.

id

Unique identifier.

public

Whether the session is publicly visible.

qrcode

URL of the QR code image for pairing cameras, or None if not available.

class Activity:

A movement recording trial with associated videos and results.

id

Unique identifier.

tags

Tags applied to this activity.

status

Raw processing status string. Use ModelHealthClient.activity_status() for the typed value.

name

Display name, or None if not set.

videos

Recorded video files associated with this activity.

results

Processed result files associated with this activity.

session

ID of the parent session.

A bare ID, not a Session object.

notes

Free-text notes attached to the activity, or None if there are none.

created_by

Username of the account that owns the parent session, or None if unknown.

activity_type

The activity type as reported, or None if unset.

Carries {id, name, display_name} through unchanged — including a type this SDK build doesn't recognize.

analysis_status

State of the analysis pipeline: one of "processing", "done", "error", or None when this activity was not loaded through a path that reports it (for example, nested inside a session).

trashed

Whether the activity is in the trash.

created_at

When this activity was created.

updated_at

When this activity was last updated.

class ActivityResult:

A processed result file associated with an activity.

id

Unique identifier.

tag

Result tag identifying the content type (e.g. "ik_results", "marker_data").

media

URL of the result file, or None if not yet available.

activity

ID of the parent activity.

class Video:

A recorded video file from an activity.

activity

ID of the parent activity.

video_thumb

URL of the video thumbnail, or None if not yet available.

video

URL of the full video, or None if not yet available.

id

Unique identifier.

class Subject:

An individual being monitored or assessed.

birth_year

Year of birth, or None if not set.

height

Height in centimeters, or None if not set.

updated_at

When this subject record was last modified, or None if not reported.

id

Unique identifier.

age

Age in years, or None if not set.

name

Display name.

weight

Weight in kilograms, or None if not set.

activity_count

Number of activities recorded for this subject, excluding calibration and neutral trials, or None if not reported.

tags

Free-form tags attached to the subject. Empty rather than missing when unset.

last_name

Family name, or None if not reported. Not always available.

last_activity

Timestamp of the subject's most recent activity, or None if they have none, or if not reported.

sex_at_birth

Sex assigned at birth as reported, or None if the subject declined to answer or no value is available.

first_name

Given name, or None if not reported. Not always available.

characteristics

Freeform text describing relevant characteristics or medical conditions.

gender

Gender identity as reported, or None if the subject declined to answer or no value is available.

created_at

When this subject record was created, or None if not reported.

class SubjectParameters:

Parameters for creating a new subject.

name

Display name.

weight

Weight in kilograms.

height

Height in centimeters.

sex_at_birth

Sex assigned at birth.

gender

Gender identity.

characteristics

Freeform text describing relevant characteristics or medical conditions.

birth_year

Year of birth, or None if not provided.

class ActivityTag:

A tag that can be applied to activities for categorization.

Use tags to organize and filter activities by type or condition (for example, "cmj", "squat", "baseline").

value

Machine-readable tag identifier.

label

Human-readable display label.

class ActivityTypeInfo:

The activity type as reported, carrying {id, name, display_name} through unchanged — including a type this SDK build doesn't otherwise recognize.

description

Freeform description, when the type carries one. None for a type reported alongside an activity, which carries only the three fields above.

id

Database id of the activity-type record.

name

The name a list filters by, e.g. "Squat Exercise".

is_custom

Whether the type belongs to the account rather than being one everybody has. None for a type reported alongside an activity.

display_name

Human-readable label. Not always the same as name — one type reads "Range of Motion (ROM)" and shows as "Custom Activity".

slug

Machine-readable key, e.g. "squat_exercise". What to branch on: it is derived from the name once and then stays put, while name and display_name can both be renamed. Empty when none was reported.

class ActivityTypesResource(_BaseResource):

The activity types this account can use.

Returned by ModelHealthClient.activity_types — do not construct directly.

def list(self) -> "'list[ActivityTypeInfo]'":

The types everybody has, plus any this account or its organisation added.

The list is the account's own and grows, which is why a list filters by a type taken from here rather than by a name known in advance.

Returns:

Every activity type available to this account.

Raises:
  • AuthenticationError: If the API key is invalid or expired.
  • NetworkError: On connection failure or timeout.
Example:
types = client.activity_types.list()
squats = next(t for t in types if t.display_name == "Squat Exercise")
for activity in client.activities.list(activity_type=squats):
    print(activity.name)
Inherited Members
_BaseResource
_BaseResource
class MotionData:

Motion data downloaded from a processed activity.

Use type to determine how to parse data.

type

The type of result data and its implicit file format.

data

The raw file bytes. Parse according to the format implied by type.

class AnalysisData:

Analysis result data downloaded from an activity with a completed analysis.

Use type to determine how to parse data.

data

The raw file bytes. Parse according to the format implied by type.

type

The type of analysis result and its implicit file format.

class Analysis:

An active analysis task returned by start_analysis.

Pass to get_analysis_status to poll for completion.

id

Unique task identifier.

class Archive:

An active archive preparation task.

id
class ExternalResultFile:

An external file to attach to an activity via ModelHealthClient.add_motion_data_to_activity().

Create instances with the tagged() factory method.

def tagged(cls, /, tag, extension, data):

Create a tagged external result file, validating the extension.

Parameters
  • tag: Identifies the data source (e.g. "acme-sensor").
  • extension: One of "csv", "json", or "binary".
  • data: Raw file bytes.
Raises
  • ValueError: If extension is not a supported format.
class LogEvent:

A single log event delivered to a handler registered via ModelHealthClient.set_log_handler().

message
timestamp_ms
category
level
code
class MotionDataType(enum.StrEnum):

Motion data types available for download from a processed activity.

Pass one or more values to ModelHealthClient.motion_data_for_activity(). Use tagged() to download an externally attached file by tag.

Example:
results = client.motion_data_for_activity(
    activity,
    [MotionDataType.kinematics_mot, MotionDataType.animation],
)

# Download an externally attached file by tag
results = client.motion_data_for_activity(
    activity, [MotionDataType.tagged("acme-sensor")]
)
data = results[0].data if results else None
animation = <MotionDataType.animation: 'animation'>

Visualisation transforms. JSON format.

kinematics_mot = <MotionDataType.kinematics_mot: 'kinematics_mot'>

Inverse-kinematics results. MOT format.

kinematics_csv = <MotionDataType.kinematics_csv: 'kinematics_csv'>

Inverse-kinematics results. CSV format.

markers_trc = <MotionDataType.markers_trc: 'markers_trc'>

Marker trajectories. TRC format.

markers_csv = <MotionDataType.markers_csv: 'markers_csv'>

Marker trajectories. CSV format.

model = <MotionDataType.model: 'model'>

OpenSim musculoskeletal model. OSim format.

@staticmethod
def tagged(tag: str) -> TaggedMotionDataType:

Return a tagged motion data descriptor for downloading an externally attached file.

Arguments:
  • tag: The tag string used when the file was uploaded.
Returns:

A TaggedMotionDataType that can be passed to ModelHealthClient.motion_data_for_activity().

class AnalysisDataType(enum.StrEnum):

Analysis result types available for download after a completed analysis.

Pass one or more values to ModelHealthClient.analysis_data_for_activity().

Example:
results = client.analysis_data_for_activity(
    activity,
    [AnalysisDataType.metrics, AnalysisDataType.report],
)
metrics = <AnalysisDataType.metrics: 'metrics'>

Computed biomechanical metrics. JSON format.

report = <AnalysisDataType.report: 'report'>

Analysis report. PDF format.

data = <AnalysisDataType.data: 'data'>

Raw analysis data. ZIP format.

class VideoVersion(enum.StrEnum):

Video version to download for an activity.

Pass to ModelHealthClient.videos_for_activity().

raw = <VideoVersion.raw: 'raw'>

Original per-camera recordings.

synced = <VideoVersion.synced: 'synced'>

Temporally-synchronised output.

class VideoUploadMode(enum.StrEnum):

Controls whether a user's devices upload recorded video.

Pass to ModelHealthClient.set_video_upload_mode().

enabled = <VideoUploadMode.enabled: 'enabled'>

Devices upload recorded video normally.

disabled = <VideoUploadMode.disabled: 'disabled'>

Devices stop uploading recorded video.

flush = <VideoUploadMode.flush: 'flush'>

Re-enables uploads, and uploads any videos that were queued locally while disabled.

class ActivityType(enum.StrEnum):

Analysis algorithm to run on an activity.

Pass to ModelHealthClient.start_analysis().

Example:
task = client.start_analysis(
    ActivityType.counter_movement_jump, activity, session
)
counter_movement_jump = <ActivityType.counter_movement_jump: 'counter_movement_jump'>
gait = <ActivityType.gait: 'gait'>
treadmill_gait = <ActivityType.treadmill_gait: 'treadmill_gait'>
treadmill_running = <ActivityType.treadmill_running: 'treadmill_running'>
overground_running = <ActivityType.overground_running: 'overground_running'>
sit_to_stand = <ActivityType.sit_to_stand: 'sit_to_stand'>
squats = <ActivityType.squats: 'squats'>
range_of_motion = <ActivityType.range_of_motion: 'range_of_motion'>
drop_jump = <ActivityType.drop_jump: 'drop_jump'>
hop = <ActivityType.hop: 'hop'>
change_of_direction = <ActivityType.change_of_direction: 'change_of_direction'>
cut = <ActivityType.cut: 'cut'>
sprint = <ActivityType.sprint: 'sprint'>
lateral_stepdown = <ActivityType.lateral_stepdown: 'lateral_stepdown'>
lunge = <ActivityType.lunge: 'lunge'>
class Gender(enum.StrEnum):

Gender identity.

Subject.gender is a plain str for backward compatibility, but its value is always one of these — use this enum instead of a literal string to get autocomplete and typo-checking.

woman = <Gender.woman: 'woman'>
man = <Gender.man: 'man'>
transgender = <Gender.transgender: 'transgender'>
non_binary = <Gender.non_binary: 'non_binary'>
no_response = <Gender.no_response: 'no_response'>

The subject declined to answer, or no value is available at all — the two are not currently distinguishable through this attribute.

class Sex(enum.StrEnum):

Sex assigned at birth.

Subject.sex_at_birth is a plain str for backward compatibility, but its value is always one of these — use this enum instead of a literal string to get autocomplete and typo-checking.

woman = <Sex.woman: 'woman'>
man = <Sex.man: 'man'>
intersex = <Sex.intersex: 'intersex'>
not_listed = <Sex.not_listed: 'not_listed'>
no_response = <Sex.no_response: 'no_response'>

The subject declined to answer, or no value is available at all — the two are not currently distinguishable through this attribute.

class UsageReason(enum.StrEnum):

Why UsageInfo's recording_allowed is False.

no_active_plan = <UsageReason.no_active_plan: 'no_active_plan'>

No plan is configured for this account.

period_expired = <UsageReason.period_expired: 'period_expired'>

The current billing period has ended.

limit_reached = <UsageReason.limit_reached: 'limit_reached'>

The activity cap for the current period has been reached.

payment_failed = <UsageReason.payment_failed: 'payment_failed'>

The most recent payment attempt failed.

class ResetPeriod(enum.StrEnum):

How often UsageInfo's activities_used resets.

monthly = <ResetPeriod.monthly: 'monthly'>
annually = <ResetPeriod.annually: 'annually'>
class ActivityConfig:

Configuration for starting or updating a recording.

Attributes:
  • activity_type: The type of activity being recorded. When set, the corresponding analysis starts automatically once the recording is processed.
  • config: Optional per-recording settings that override the session-level config.
  • add_tags: Tags to add to the activity. Merged with existing tags.
  • remove_tags: Tags to remove from the activity.
  • name: New display name to set on the activity.
ActivityConfig( activity_type: Optional[ActivityType] = None, config: Optional[RecordingConfig] = None, add_tags: Optional[list[str]] = None, remove_tags: Optional[list[str]] = None, name: Optional[str] = None)
activity_type: Optional[ActivityType]
config: Optional[RecordingConfig]
add_tags: list[str]
remove_tags: list[str]
name: Optional[str]
@dataclass
class RecordingConfig:

Per-recording settings that override session-level config.

All fields are optional — omitted fields fall back to the session's configured values.

Attributes:
  • framerate: Camera frame rate override.
  • filter_frequency: Low-pass filter frequency override.
Example:
from modelhealth import ActivityConfig, RecordingConfig, SessionFramerate

config = ActivityConfig(
    ActivityType.counter_movement_jump,
    config=RecordingConfig(framerate=SessionFramerate.fps_60),
)
activity = client.start_recording("cmj", session, config)
RecordingConfig( framerate: Optional[SessionFramerate] = None, filter_frequency: Union[FilterFrequencyDefault, FilterFrequencyHz, NoneType] = None)
framerate: Optional[SessionFramerate] = None

Camera frame rate override. None uses the session default.

filter_frequency: Union[FilterFrequencyDefault, FilterFrequencyHz, NoneType] = None

Low-pass filter frequency override. None uses the session default.

class CheckerboardPlacement(enum.StrEnum):

Orientation of the calibration checkerboard relative to the ground.

Pass to CheckerboardDetails.

perpendicular = <CheckerboardPlacement.perpendicular: 'perpendicular'>

Checkerboard upright, plane perpendicular to the ground.

parallel = <CheckerboardPlacement.parallel: 'parallel'>

Checkerboard flat on the floor, plane parallel to the ground.

class LogLevel(enum.StrEnum):

How verbose a log event stream should be.

Pass to ModelHealthClient.set_log_handler() or ModelHealthClient.set_log_level(). Each level includes every level above it: warn includes error, info includes both.

off = <LogLevel.off: 'off'>

No log events are delivered.

error = <LogLevel.error: 'error'>

Only error-level events.

warn = <LogLevel.warn: 'warn'>

Warning-level events and above.

info = <LogLevel.info: 'info'>

All log events, including informational ones.

class LogCategory(enum.StrEnum):

Which part of a session's lifecycle a log event describes.

See LogEvent.

account = <LogCategory.account: 'account'>

Account and profile events, such as fetching account info or changing settings.

activity = <LogCategory.activity: 'activity'>

Activity lifecycle events, such as fetching, updating, or deleting one.

analysis = <LogCategory.analysis: 'analysis'>

Analysis lifecycle events.

metrics = <LogCategory.metrics: 'metrics'>

Metrics events, such as fetching dashboard metrics.

sdk = <LogCategory.sdk: 'sdk'>

Client-level events, such as initialization.

session = <LogCategory.session: 'session'>

Session, recording, and calibration lifecycle events.

subject = <LogCategory.subject: 'subject'>

Subject lifecycle events, such as fetching or creating one.

video = <LogCategory.video: 'video'>

Video download events.

class ActivityStatus(enum.StrEnum):

The processing state of an activity (non-uploading variants).

Activities must reach ready before analysis can begin. When videos are still uploading, ModelHealthClient.activity_status() returns ActivityStatusUploading instead.

Poll ModelHealthClient.activity_status() after recording stops to track upload and processing progress. Once ready, pass the activity to ModelHealthClient.start_analysis().

Example:
import time

status = client.activity_status(activity)
while status == ActivityStatus.processing:
    time.sleep(5)
    status = client.activity_status(activity)

if status == ActivityStatus.ready:
    task = client.start_analysis(ActivityType.counter_movement_jump, activity, session)
elif status == ActivityStatus.failed:
    print("Processing failed")
processing = <ActivityStatus.processing: 'processing'>

Videos have been uploaded and are being processed.

ready = <ActivityStatus.ready: 'ready'>

Processing is complete. The activity is ready for analysis.

failed = <ActivityStatus.failed: 'failed'>

Processing failed.

@dataclass(frozen=True)
class ActivityStatusUploading:

Videos are being uploaded from cameras.

Returned by ModelHealthClient.activity_status() when upload is in progress. uploaded and total track progress.

Example:
status = client.activity_status(activity)
if isinstance(status, ActivityStatusUploading):
    print(f"Uploading: {status.uploaded}/{status.total}")
ActivityStatusUploading(uploaded: int, total: int)
uploaded: int

Number of videos successfully uploaded so far.

total: int

Total number of videos expected from all cameras.

@dataclass(frozen=True)
class ActivityStatusAnalyzing:

Analysis has been triggered and is in progress.

Returned by ModelHealthClient.activity_status() after processing completes when an activity type was set at recording time. Pass task to ModelHealthClient.analysis_status() to track analysis progress.

Example:
status = client.activity_status(activity)
if isinstance(status, ActivityStatusAnalyzing):
    analysis_status = client.analysis_status(status.task)
ActivityStatusAnalyzing(task: Analysis)
task: Analysis

The analysis task. Pass to ModelHealthClient.analysis_status().

class AnalysisStatus(enum.StrEnum):

The current state of an analysis task.

Poll ModelHealthClient.analysis_status() after calling ModelHealthClient.start_analysis() to monitor progress. When status reaches completed, download results with ModelHealthClient.analysis_data_for_activity().

Example:
import time

status = client.analysis_status(task)
while status == AnalysisStatus.processing:
    time.sleep(10)
    status = client.analysis_status(task)

if status == AnalysisStatus.completed:
    results = client.analysis_data_for_activity(activity, [AnalysisDataType.metrics])
processing = <AnalysisStatus.processing: 'processing'>

Analysis is in progress.

completed = <AnalysisStatus.completed: 'completed'>

Analysis completed successfully.

failed = <AnalysisStatus.failed: 'failed'>

Analysis failed.

class ArchiveStatus(enum.StrEnum):

The current state of an archive preparation task.

Poll ModelHealthClient.archive_status() after calling ModelHealthClient.prepare_archive() to monitor progress. When status reaches ready, download the archive with ModelHealthClient.archive_data().

Example:
import time

archive = client.prepare_archive(session)
status = client.archive_status(archive)
while status == ArchiveStatus.processing:
    time.sleep(5)
    status = client.archive_status(archive)

if status == ArchiveStatus.ready:
    data = client.archive_data(archive)
processing = <ArchiveStatus.processing: 'processing'>

Archive is being prepared.

ready = <ArchiveStatus.ready: 'ready'>

Archive is ready to download.

failed = <ArchiveStatus.failed: 'failed'>

Archive preparation failed.

class CalibrationStatus(enum.StrEnum):

The current state of a calibration process (non-uploading, non-processing variants).

When videos are uploading, ModelHealthClient.calibrate_camera() and ModelHealthClient.calibrate_subject() pass CalibrationStatusUploading to the callback. When the videos are being processed, CalibrationStatusProcessing is passed instead.

recording = <CalibrationStatus.recording: 'recording'>

All connected cameras are actively recording.

done = <CalibrationStatus.done: 'done'>

Calibration completed successfully.

@dataclass(frozen=True)
class CalibrationStatusUploading:

Videos are being uploaded from cameras.

Passed to the status callback when upload is in progress. uploaded and total track progress.

CalibrationStatusUploading(uploaded: int, total: int)
uploaded: int

Number of videos successfully uploaded so far.

total: int

Total number of videos expected from all cameras.

@dataclass(frozen=True)
class CalibrationStatusProcessing:

The uploaded videos are being processed.

Passed to the status callback while processing is underway. percent is the completion percentage, or None if unavailable.

CalibrationStatusProcessing(percent: int | None)
percent: int | None

Processing completion percentage (0–100), or None if unavailable.

class CheckerboardDetails:

Configuration for a calibration checkerboard pattern.

rows

Number of internal corner rows.

placement

Checkerboard orientation: "perpendicular" or "parallel".

columns

Number of internal corner columns.

square_size

Size of each square in millimeters.

@dataclass
class SessionConfig:

Settings to apply to a session before recording.

All fields have sensible defaults. Pass to ModelHealthClient.import_session() (or call ModelHealthClient.configure_session() directly) to override.

Example:
from modelhealth import SessionConfig, SessionFramerate, SessionDataSharing

config = SessionConfig(
    framerate=SessionFramerate.fps_60,
    data_sharing=SessionDataSharing.share_no_data,
)
session = client.import_session(activities_json, subject, session_config=config)
SessionConfig( framerate: SessionFramerate = <SessionFramerate.fps_120: 'fps_120'>, opensim_model: SessionOpenSimModel = <SessionOpenSimModel.lai_uhlrich_2022: 'lai_uhlrich_2022'>, scaling_setup: SessionScalingSetup = <SessionScalingSetup.upright_standing_pose: 'upright_standing_pose'>, core_engine: SessionCoreEngine = <SessionCoreEngine.v1_0: 'v1_0'>, filter_frequency: Union[FilterFrequencyDefault, FilterFrequencyHz] = None, data_sharing: SessionDataSharing = <SessionDataSharing.share_processed_data_and_identified_videos: 'share_processed_data_and_identified_videos'>)
framerate: SessionFramerate = <SessionFramerate.fps_120: 'fps_120'>

Camera frame rate. Default: fps_120.

opensim_model: SessionOpenSimModel = <SessionOpenSimModel.lai_uhlrich_2022: 'lai_uhlrich_2022'>

OpenSim musculoskeletal model. Default: lai_uhlrich_2022.

scaling_setup: SessionScalingSetup = <SessionScalingSetup.upright_standing_pose: 'upright_standing_pose'>

Scaling pose. Default: upright_standing_pose.

core_engine: SessionCoreEngine = <SessionCoreEngine.v1_0: 'v1_0'>

Core processing engine version. Default: v1_0.

filter_frequency: Union[FilterFrequencyDefault, FilterFrequencyHz] = None

Low-pass filter frequency. Default: chosen automatically.

data_sharing: SessionDataSharing = <SessionDataSharing.share_processed_data_and_identified_videos: 'share_processed_data_and_identified_videos'>

Data-sharing preference. Default: share_processed_data_and_identified_videos.

class SessionFramerate(enum.StrEnum):

Camera frame rate for a recording session.

Higher framerates capture fast movements more accurately but increase processing time proportionally. Collect only as much footage as needed and keep recordings under the suggested durations.

Pass to ModelHealthClient.configure_session().

fps_60 = <SessionFramerate.fps_60: 'fps_60'>

60 frames per second. Suggested maximum recording duration: 60 s.

fps_120 = <SessionFramerate.fps_120: 'fps_120'>

120 frames per second (default). Suggested maximum recording duration: 30 s.

fps_240 = <SessionFramerate.fps_240: 'fps_240'>

240 frames per second. Suggested maximum recording duration: 15 s.

class SessionOpenSimModel(enum.StrEnum):

OpenSim musculoskeletal model used for biomechanical analysis.

Pass to ModelHealthClient.configure_session().

lai_uhlrich_2022_shoulder = <SessionOpenSimModel.lai_uhlrich_2022_shoulder: 'lai_uhlrich_2022_shoulder'>

Full-body model with 33 degrees of freedom plus a 6-DoF shoulder complex with a scapulothoracic body and a glenohumeral joint using the ISB-recommended Y-X-Y rotation sequence.

lai_uhlrich_2022 = <SessionOpenSimModel.lai_uhlrich_2022: 'lai_uhlrich_2022'>

Same full-body model as lai_uhlrich_2022_shoulder without the ISB shoulder complex. Default.

class SessionScalingSetup(enum.StrEnum):

Pose used for subject scaling during calibration.

Pass to ModelHealthClient.configure_session().

upright_standing_pose = <SessionScalingSetup.upright_standing_pose: 'upright_standing_pose'>

Subject stands straight with feet pointing forward and no bending or rotation at the hips, knees, or ankles. Default.

any_pose = <SessionScalingSetup.any_pose: 'any_pose'>

No posture assumptions. Use when the subject cannot adopt the upright standing pose. Requires all body segments to be visible by at least two cameras.

class SessionCoreEngine(enum.StrEnum):

Core processing engine version.

Pass to ModelHealthClient.configure_session().

v0_2 = <SessionCoreEngine.v0_2: 'v0_2'>

Version 0.2.

v0_3 = <SessionCoreEngine.v0_3: 'v0_3'>

Version 0.3.

v1_0 = <SessionCoreEngine.v1_0: 'v1_0'>

Version 1.0 (default).

class SessionDataSharing(enum.StrEnum):

Data-sharing preference for a session.

Session data and videos are uploaded securely to Model Health for processing. This setting controls what Model Health can use for internal development. Identified videos contain original footage with faces unblurred; de-identified videos have faces blurred. Processed data (e.g. joint angles) is always de-identified.

Pass to ModelHealthClient.configure_session().

share_processed_data_and_identified_videos = <SessionDataSharing.share_processed_data_and_identified_videos: 'share_processed_data_and_identified_videos'>

Share processed data and identified videos (default).

share_processed_data_and_deidentified_videos = <SessionDataSharing.share_processed_data_and_deidentified_videos: 'share_processed_data_and_deidentified_videos'>

Share processed data and de-identified videos (faces blurred).

share_processed_data = <SessionDataSharing.share_processed_data: 'share_processed_data'>

Share processed data only. No videos are shared.

share_no_data = <SessionDataSharing.share_no_data: 'share_no_data'>

Share no data for internal development.

class FilterFrequencyDefault:

Use the automatically chosen filter frequency.

class FilterFrequencyHz:

Use a specific filter frequency in Hz.

value

Frequency in Hz.

@dataclass(frozen=True)
class ImportStatusCreatedSession:

Session created successfully during a session import.

Passed to the status callback during ModelHealthClient.import_session() once the new session has been created.

ImportStatusCreatedSession(session_id: str)
session_id: str

ID of the newly created session.

@dataclass(frozen=True)
class ImportStatusUploading:

A video is being uploaded during a session import.

Passed to the status callback during ModelHealthClient.import_session() while videos are being transferred. trial names the current trial; uploaded and total track progress within that trial.

ImportStatusUploading(trial: str, uploaded: int, total: int)
trial: str

Name of the trial whose video is being uploaded.

uploaded: int

Number of videos successfully uploaded so far.

total: int

Total number of videos to upload.

class MetricValueScalar:

A scalar (unilateral) metric measurement.

value

The measured value, or None if not yet computed.

class MetricValueBilateral:

A bilateral metric measurement with independent left and right values.

right

Right-side measurement, or None if not yet computed.

left

Left-side measurement, or None if not yet computed.

class Metric:

A single computed biomechanical metric for one activity.

name

Display name.

description

Human-readable description, or None if not set.

class MetricsGroup:

A category group that organizes related metrics on a dashboard.

metrics

Metrics within this group.

name

Display name.

description

Human-readable description, or None if not set.

class ActivityMetrics:

All dashboard metrics for a single activity, organized into category groups.

def to_json(self, /, pretty=True):

Serialize these metrics to a JSON string (snake_case keys, matching the wire format).

Pretty-printed (indented) by default; pass pretty=False for compact output.

groups

Metric groups in display order.

activity_type_id

Database ID of the activity type.

activity_id

UUID of the activity these metrics belong to.