Metadata-Version: 2.5
Name: interhumanai
Version: 1.3.2
Summary: First-party Python SDK for the Interhuman API: upload and stream 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 WebSocket protocol 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 surface is
inherently event-driven, so a single async API keeps every surface consistent
(and mirrors the promise-based TypeScript SDK).

## Quickstart

```python
import asyncio
import os

from interhumanai import InterhumanClient, UploadJobIncludeFlag, UploadJobStatus, UploadModel

async def main() -> None:
    client = InterhumanClient(access_token=os.environ["INTERHUMAN_API_KEY"])
    job = await client.upload.submit(
        "meeting.mp4", model=UploadModel.INTER_2, include=[UploadJobIncludeFlag.SIGNALS]
    )
    job = await client.upload.wait_for_job(job)
    if job.status is UploadJobStatus.COMPLETED:
        for signal in job.result.signals:
            print(signal.type.value, signal.start, signal.end)
    else:
        print(job.error.error_id, job.error.message)

asyncio.run(main())
```

For a live feed, open `client.stream(api_version="v2")` (see [Stream API](#stream-api)).
Pass `api_version` explicitly: the default is `"v1"`.

## Authentication

Pass a credential as `access_token`; the client sends it as-is:

- **Your API key**, on your server.
- **A client token** (see below) on an untrusted client such as a browser,
  which must never see the API key.

```python
import os

from interhumanai import InterhumanClient

# On your server
client = InterhumanClient(access_token=os.environ["INTERHUMAN_API_KEY"])

# A client token minted by your backend
client = InterhumanClient(access_token=client_token)
```

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

Scopes name one endpoint family, on every API version that serves it:

- `Scope.UPLOAD` (`interhumanai.upload`) — `POST /v1/upload/analyze`, and
  `POST /v2/upload/analyze` with every model it serves; also reads the
  account's jobs.
- `Scope.STREAM` (`interhumanai.stream`) — `WS /v1/stream/analyze`, and
  `WS /v2/stream/analyze` with every model it serves.

A scope covers its endpoint on every API version and every model. Holding one
scope never implies another, so request every scope the client needs.

## 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

`client.upload.submit()` sends a file to `POST /v2/upload/analyze`, which
analyzes it 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 UploadJobIncludeFlag, UploadModel

job = await client.upload.submit(
    "meeting.mp4",
    model=UploadModel.INTER_2,
    # optional whole-file views: quality scores and the merged signal list
    include=[UploadJobIncludeFlag.CONVERSATION_QUALITY_OVERALL, UploadJobIncludeFlag.SIGNALS],
)
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}")
    for signal in job.result.signals:
        print(signal.type.value, signal.start, signal.end)
    print(job.result.conversation_quality.overall.quality_index)
else:
    print(job.error.error_id, job.error.message)
```

- `submit(file, model=..., wait_seconds=0, include=None)` — 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. Pass `include` with `UploadJobIncludeFlag` values to have
  the result carry whole-file views. The two Conversation Quality Index values
  (the same ones `analyze` takes as `IncludeFlag`, whose members also work
  here) add `conversation_quality`, an `UploadJobConversationQuality` scored
  over the whole file with `overall` and/or `timeline` set for the flags you
  named. `SIGNALS` adds `signals`: every window's signals as one chronological
  list, same-type signals in adjacent windows merged into one span with the
  highest probability and its rationale, and no `modality` (an empty list when
  nothing was detected). Without a flag the result has no such section.
- `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` accepts an mp4, mov, avi, mkv, webm or mpeg-ts video
  carrying both a video and an audio track and reads picture and sound
  together. It takes 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. Each signal is an
  `UploadJobSignal`: the v1 `Signal` without `modality`.
- `UploadModel.INTER_2_AUDIO` reads the audio alone: a wav, flac, mp3, m4a or
  ogg file, or a webm or mp4 carrying an audio track, under the same limits.
  It reports the same result shape, with a subset of the signals
  `UploadModel.INTER_2` reports: agreement, confidence, frustration,
  hesitation, interest and uncertainty. The same `Scope.UPLOAD` covers it.

### Inter-1 upload (V1, compatibility)

`client.upload.analyze()` calls `POST /v1/upload/analyze`, which analyzes the
file with Inter-1 and answers synchronously. Use it only to keep an existing
V1 integration running; see
[Migrate from V1 to V2](https://docs.interhuman.ai/api-reference/migrate-v1-to-v2).
It takes 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 v1 `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"]`. The Inter-2 (v2) routes send
no `modality`: the model you choose names the evidence.

## Stream API

One `StreamClient` handles one live session. Pass `api_version="v2"` to open
`WS /v2/stream/analyze`, analyzed by Inter-2. Send binary WebM or
fragmented-MP4 chunks as your recorder produces them, and consume typed events
with `async for`:

```python
from interhumanai import IncludeFlag, SignalDetectedEvent

async with client.stream(api_version="v2") 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()` without `api_version` opens `WS /v1/stream/analyze`,
analyzed by the Inter-1 model, so always pass `api_version="v2"` for Inter-2.
Use V1 only to keep an existing integration running.

Apart from the endpoint and the model, V1 and V2 sessions are identical: the
`session.ready` limits, session config, the video you send, the events you
receive and their order, the same `Scope.STREAM` scope for every model, and
the graceful close. Code written for V1 therefore works on V2 unchanged,
except that v2 signal events carry no `modality` (`event.data.modality` is
`None`). Errors raised for a v2 session also 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.

### Choosing the Inter-2 model

On `api_version="v2"` the `model` argument names the model the session opens
on. It defaults to `StreamModel.INTER_2`, which reads the video;
`Scope.STREAM` covers every model. `session.ready` lists the models the deployment serves
that your credential may select under `supported_session_config_options.model`,
and `session.updated` echoes the one in force.

```python
from interhumanai import StreamModel

async with client.stream(api_version="v2", model=StreamModel.INTER_2) as session:
    await session.wait_for_session_ready()
```

`StreamModel.INTER_2_AUDIO` hears the audio alone and reports a subset of the
signals `StreamModel.INTER_2` reports (agreement, confidence, frustration,
hesitation, interest, uncertainty). It takes the same WebM or fragmented-MP4
clips through `send_video`, with or without a video track, so an audio-only
recording works as-is. To send raw audio instead, declare the format once in
the session config and then send samples with `send_audio`: signed 16-bit
little-endian mono PCM at 16000 or 24000 Hz, cut at any byte boundary.

```python
from interhumanai import RawAudioFormat, StreamModel

async with client.stream(api_version="v2", model=StreamModel.INTER_2_AUDIO) as session:
    await session.wait_for_session_ready()
    await session.update_config(audio_format=RawAudioFormat(sample_rate=16000))
    await session.send_audio(pcm_frame)
```

## 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 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(access_token=api_key)                                   # production (default): api.interhuman.ai
InterhumanClient(access_token=api_key, environment="staging")            # staging-api.interhuman.ai
InterhumanClient(access_token=api_key, base_url="http://localhost:8080") # local override
```

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

## Examples

Runnable scripts for every flow live in [`examples/`](examples/):
`upload_job.py` (Inter-2 upload), `stream.py` (Inter-2 stream),
`client_tokens.py`, and `upload.py` (V1 compatibility).

## 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
