Metadata-Version: 2.5
Name: interhumanai
Version: 0.16.0
Summary: First-party Python SDK for the Interhuman API: upload, stream, and realtime social-signal analysis.
Project-URL: Homepage, https://docs.interhuman.ai
Project-URL: Repository, https://github.com/interhumanai/interhuman-api
Project-URL: Changelog, https://github.com/interhumanai/interhuman-api/blob/main/sdk/python/CHANGELOG.md
Author: Interhuman AI
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: analysis,interhuman,sdk,social-signals,video
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27.0
Requires-Dist: pydantic>=2.7.0
Requires-Dist: websockets>=13.0
Description-Content-Type: text/markdown

# Interhuman Python SDK

`interhumanai` is the first-party Python client for the
[Interhuman API](https://docs.interhuman.ai). It wraps authentication, client
tokens, video upload analysis, and the live stream and realtime WebSocket
protocols behind a typed, asyncio-first API — no hand-rolled token exchange,
multipart bodies, or WebSocket envelope parsing.

## Installation

```bash
pip install interhumanai
```

Requires Python 3.10+. Runtime dependencies: `httpx`, `websockets`, `pydantic`.

The SDK is asyncio-first: every network call is a coroutine. Use `asyncio.run()`
in scripts and top-level `await` in notebooks. The live stream and realtime
surfaces are inherently event-driven, so a single async API keeps every surface
consistent (and mirrors the promise-based TypeScript SDK).

## Quickstart

```python
import asyncio
from interhumanai import InterhumanClient

async def main() -> None:
    client = InterhumanClient(key_id="...", key_secret="...")
    result = await client.upload.analyze("meeting.mp4")
    for signal in result.signals:
        print(signal.type.value, signal.start, signal.end)

asyncio.run(main())
```

## Authentication

Two ways to authenticate:

- **API key credentials** (`key_id` + `key_secret`): the client exchanges them
  at `POST /v1/auth` for a short-lived bearer token and refreshes it
  automatically before expiry.
- **Pre-issued token** (`access_token`): a JWT or client token used as-is.

```python
from interhumanai import InterhumanClient, Scope

# Managed credentials (recommended for servers)
client = InterhumanClient(key_id="...", key_secret="...", scopes=[Scope.UPLOAD, Scope.STREAM])

# Pre-issued bearer token
client = InterhumanClient(access_token="eyJ...")
```

The standalone `AuthClient` exposes the token endpoints directly, and
`TokenManager` / `StaticTokenProvider` are available when you need to plug a
custom token source into the lower-level clients.

## Client tokens (browser / untrusted clients)

Mint short-lived, capped tokens server-side so untrusted clients never see the
API key:

```python
from interhumanai import AuthClient, Scope

auth = AuthClient()
token = await auth.create_client_token(
    api_key="ih_...",
    scopes=[Scope.STREAM],
    expires_in=300,            # clamped to 60-3600 seconds by the API
    max_concurrent=1,
    max_video_seconds=600,
    allowed_origins=["https://app.example.com"],
)
await auth.revoke_client_token(api_key="ih_...", token=token.access_token)
```

## Upload API

Analyze a complete video file (mp4, avi, mov, mkv, mpeg-ts, or webm; at least
3 seconds, at most 32 MB):

```python
from interhumanai import GoalDimension, IncludeFlag

result = await client.upload.analyze(
    "meeting.mp4",                                        # path, bytes, or file object
    include=[IncludeFlag.CONVERSATION_QUALITY_OVERALL],   # optional sections
    goal_dimensions=[GoalDimension.CLARITY],              # enables feedback
)
```

The typed `AnalysisResult` carries `signals`, `engagement_state`, and — when
requested — `feedback` and `conversation_quality`.

Every `Signal` carries a `modality` naming the analysis modalities that
detected it. When several tracks detect the same signal, every contributing
modality is included, e.g. `["audio", "visual"]`.

### Upload jobs with Inter-2 (`POST /v2/upload/analyze`)

The v2 upload route analyzes a file with an Inter-2 model as an **asynchronous
job**: the API validates the file, answers with a job envelope, and analyzes
the file afterwards. Poll the job until it is `completed` or `failed`:

```python
from interhumanai import UploadModel

job = await client.upload.submit("call.wav", model=UploadModel.INTER_2_AUDIO)
print(job.job_id, job.status.value)          # queued

job = await client.upload.wait_for_job(job, timeout=300)   # polls every 2 s
if job.status.value == "completed":
    for window in job.result.windows:
        names = ", ".join(signal.type.value for signal in window.signals)
        print(f"{window.start_seconds:5.1f}s {window.engagement_status.value:11} {names}")
else:
    print(job.error.error_id, job.error.message)
```

- `submit(file, model=..., wait_seconds=0)` — pass `wait_seconds` to let the
  API hold the request open for up to that long and return the finished job
  inline when it completes in time; otherwise it returns the queued envelope.
- `get_job(job_id)` — one status read. Raises `InterhumanAPIError` with
  `error_id` `ih4021` once the job has expired (`job.expires_at`).
- `wait_for_job(job_id, timeout=None, poll_interval=2.0)` — returns the
  terminal envelope, failed jobs included; raises `UploadJobTimeoutError` when
  `timeout` elapses first (the job keeps running).
- `UploadModel.INTER_2_AUDIO` accepts wav, flac, mp3, m4a, ogg, and webm or
  mp4 with an audio track — at least 3 seconds, at most 32 MB, and no longer
  than the deployment's maximum duration (30 minutes by default) — and reports
  one `UploadJobWindow` per fixed-length window, each with its span, an
  `engagement_status` and the `signals` read over it (`modality` `["audio"]`).
  `UploadModel.INTER_2` and `UploadModel.INTER_2_DEEP` are valid values the
  route does not serve yet and raise `InterhumanAPIError` (`ih4020`).

## Stream API

One `StreamClient` handles one live session against `WS /v1/stream/analyze`.
Send binary WebM or fragmented-MP4 chunks and consume typed events with
`async for`:

```python
from interhumanai import IncludeFlag, SignalDetectedEvent

async with client.stream() as session:
    await session.wait_for_session_ready()
    await session.update_config(include=[IncludeFlag.CONVERSATION_QUALITY_OVERALL])
    await session.send_video(first_chunk)   # first chunk carries the container header
    await session.request_close()           # graceful drain; close() tears down immediately
    async for event in session:
        if isinstance(event, SignalDetectedEvent):
            print(event.data.signal_type.value, event.data.start)
```

Iteration ends when the connection closes; `session.close_info` then holds the
close code and reason. Event types this SDK version does not know arrive as
`UnknownEvent` instead of failing the session.

### Choosing the model: v1 and v2

`client.stream()` opens `WS /v1/stream/analyze`, analyzed by the Inter-1
model. Pass `api_version="v2"` to open `WS /v2/stream/analyze` and have the
same session analyzed by the Inter-2 model:

```python
async with client.stream(api_version="v2") as session:
    ...
```

Everything else is identical — the scope, the `session.ready` limits, session
config, the video you send, the events you receive and their order, and the
graceful close — so the code above works unchanged. The one difference is how
the client names itself: errors raised for a v2 session say "Inter-2 stream"
where a v1 session says "Stream", so a message names the endpoint the session
actually opened. If a deployment has no
Inter-2 backend configured, a v2 session is refused right after the handshake
with an `ErrorEvent` (`ih1003`) and close code 1013.

## Realtime API

`RealtimeClient` targets `WS /v0/realtime/analyze`, which requires the
`interhumanai.realtime` scope, so include `Scope.REALTIME` in the scopes the
client requests alongside any others you use (a token grants exactly what was
requested, so `[Scope.REALTIME]` alone would close off `upload()` and
`stream()`). It offers multi-track analysis
configuration, client transcripts, and periodic recommendations:

```python
from interhumanai import AnalysisGroup, RealtimeRecommendationFrequency, RealtimeRecommendationGeneratedEvent, Scope, TranscriptSegment

client = InterhumanClient(
    key_id=..., key_secret=..., scopes=[Scope.UPLOAD, Scope.STREAM, Scope.REALTIME]
)

async with client.realtime() as session:
    await session.wait_for_session_ready()
    await session.update_config(
        analysis_groups=[AnalysisGroup.AUDIO, AnalysisGroup.VISUAL],
        realtime_recommendation_frequency=RealtimeRecommendationFrequency.MEDIUM,
        realtime_recommendation_instructions="Coach the presenter.",   # non-empty value enables recommendations
    )
    await session.send_transcript([TranscriptSegment(start=0.0, end=2.0, text="Hi.", speaker=0)])
    async for event in session:
        if isinstance(event, RealtimeRecommendationGeneratedEvent):
            print(event.data.text)
```

The `VISUAL` analysis group reports its negative signal as
`SignalType.TENSION`, while the `AUDIO` group (and Inter-1) reports
`SignalType.FRUSTRATION`. They describe the same state seen versus heard, and
both can be active at once, each with its own signal lifecycle.
`SignalType.TENSION` is realtime only — the upload and stream surfaces never
emit it — so code that branches on `signal_type` should handle both.

## Errors

Three surfaces, mirroring the API:

- `InterhumanAPIError` — raised for non-2xx HTTP responses (with `status`,
  `error_id`, `correlation_id`, `link`) and for transport failures
  (`status == 0`).
- `InterhumanConfigError` — raised for client-side misuse before any network
  call (missing credentials, sending on a closed session, double connect).
- `wait_for_session_ready()` raises `InterhumanError` if the connection closes
  before the session becomes ready. When the server explained the close with an
  `error` envelope first, the message quotes it; otherwise it points at the
  credential or scope, which is the usual cause.
- WebSocket `error` **envelopes** are delivered as `ErrorEvent`s through
  iteration, not raised — fatal ones are followed by the connection closing.

## SDK attribution

Every request the SDK makes tells the API which SDK sent it, so Interhuman can
report SDK adoption and spot clients stuck on an old release:

- HTTP requests carry `X-Interhuman-SDK: python/<version>`.
- Stream and realtime WebSocket handshakes carry the same header **and**
  `ih_sdk=python&ih_sdk_version=<version>` in the URL; the two always agree,
  and if an intermediary makes them disagree the API keeps the header's
  identity. The query pair is what survives an intermediary that does not
  preserve handshake headers. Any query parameters your `base_url` already had
  are kept. (Releases before 0.15.0 sent the same values as
  `sdk`/`sdk_version`; the API accepts both spellings.)

The version comes from the installed package, so it always matches the release
you have. Nothing else is sent — no device, OS, runtime, hostname, application
name, or end-user identifier — and because any caller can send the same values,
the API treats the metadata as a self-declared hint that never affects
authentication, authorization, quotas, or billing. Servers that predate it
ignore it.

`SDK_NAME`, `SDK_HEADER_NAME`, and `sdk_header_value()` are exported if you
want to inspect exactly what is sent.

## Environments

```python
InterhumanClient(key_id=..., key_secret=...)                          # production (default): api.interhuman.ai
InterhumanClient(key_id=..., key_secret=..., environment="staging")   # staging-api.interhuman.ai
InterhumanClient(key_id=..., key_secret=..., base_url="http://localhost:8080")  # local override
```

WebSocket URLs are derived automatically (`https://` → `wss://`).

## Examples

Runnable scripts for every flow live in [`examples/`](examples/): `auth.py`,
`client_tokens.py`, `upload.py`, `upload_job.py`, `stream.py`, and
`realtime.py`.

## Development

The SDK lives in the [interhuman-api](https://github.com/interhumanai/interhuman-api)
repository under `sdk/python/`. Its tests live in `tests/sdk/python/` and run
with the repository's main suite:

```bash
uv run pytest tests/sdk/python
```

### API reference docs

The reference documentation on [docs.interhuman.ai](https://docs.interhuman.ai)
is generated from this package's docstrings and type metadata — do not hand-write
it. Edit the docstrings in `src/interhumanai/`, then regenerate:

```bash
python scripts/build_mintlify_docs.py   # docstrings → Mintlify MDX in docs-dist/mintlify/
```

On every push to `main` that touches the SDK, the `Sync Docs to interhuman-docs`
workflow regenerates this page and opens a PR against the `interhuman-docs`
repository. That workflow keeps a single rolling docs-sync PR open — a later run
updates it in place instead of opening another one.

## Releasing

This package is versioned in **lockstep** with the TypeScript SDK
(`@interhumanai/sdk`): both always carry the same version and release together
from a single workflow. Publishing is automatic and deploy-gated: bump
`__version__` in `src/interhumanai/_version.py` **and** the `version` in
`sdk/typescript/package.json` to the same value (semver), add a `CHANGELOG.md`
entry to each package, and merge to `main`. After the `Deploy` workflow
succeeds, the `SDK release` workflow builds, tests, and publishes both packages
(idempotently per registry), then tags the commit `sdk-v<version>`. See
[`docs/sdk-release.md`](../../docs/sdk-release.md) for details.

## License

Apache-2.0
