interhumanai 1.3.0__tar.gz → 1.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (20) hide show
  1. {interhumanai-1.3.0 → interhumanai-1.3.2}/CHANGELOG.md +20 -0
  2. {interhumanai-1.3.0 → interhumanai-1.3.2}/PKG-INFO +111 -75
  3. {interhumanai-1.3.0 → interhumanai-1.3.2}/README.md +110 -74
  4. {interhumanai-1.3.0 → interhumanai-1.3.2}/src/interhumanai/__init__.py +17 -5
  5. {interhumanai-1.3.0 → interhumanai-1.3.2}/src/interhumanai/_version.py +1 -1
  6. {interhumanai-1.3.0 → interhumanai-1.3.2}/src/interhumanai/client.py +5 -5
  7. {interhumanai-1.3.0 → interhumanai-1.3.2}/src/interhumanai/stream.py +16 -16
  8. {interhumanai-1.3.0 → interhumanai-1.3.2}/src/interhumanai/types.py +22 -8
  9. {interhumanai-1.3.0 → interhumanai-1.3.2}/.gitignore +0 -0
  10. {interhumanai-1.3.0 → interhumanai-1.3.2}/LICENSE +0 -0
  11. {interhumanai-1.3.0 → interhumanai-1.3.2}/pyproject.toml +0 -0
  12. {interhumanai-1.3.0 → interhumanai-1.3.2}/src/interhumanai/_http.py +0 -0
  13. {interhumanai-1.3.0 → interhumanai-1.3.2}/src/interhumanai/_sdk_metadata.py +0 -0
  14. {interhumanai-1.3.0 → interhumanai-1.3.2}/src/interhumanai/_session_socket.py +0 -0
  15. {interhumanai-1.3.0 → interhumanai-1.3.2}/src/interhumanai/auth.py +0 -0
  16. {interhumanai-1.3.0 → interhumanai-1.3.2}/src/interhumanai/config.py +0 -0
  17. {interhumanai-1.3.0 → interhumanai-1.3.2}/src/interhumanai/errors.py +0 -0
  18. {interhumanai-1.3.0 → interhumanai-1.3.2}/src/interhumanai/py.typed +0 -0
  19. {interhumanai-1.3.0 → interhumanai-1.3.2}/src/interhumanai/realtime.py +0 -0
  20. {interhumanai-1.3.0 → interhumanai-1.3.2}/src/interhumanai/upload.py +0 -0
@@ -5,6 +5,26 @@ All notable changes to `interhumanai` are documented here. The package follows
5
5
  the TypeScript SDK (`@interhumanai/sdk`): both packages always carry the same
6
6
  version and release together.
7
7
 
8
+ ## 1.3.2
9
+
10
+ - ``Scope.UPLOAD`` and ``Scope.STREAM`` now cover every model their endpoint
11
+ serves. The connect-rejection hint of a v2 stream session names the stream
12
+ scope whichever model the session opens on.
13
+ - Documented the Inter-2 Audio model: ``UploadModel.INTER_2_AUDIO``,
14
+ ``StreamModel.INTER_2_AUDIO``, ``update_config(audio_format=...)``
15
+ (``RawAudioFormat``) and ``send_audio`` are part of the generated reference,
16
+ with the file types each model accepts and the signal subset the audio
17
+ model reports. No behavior change.
18
+
19
+ ## 1.3.1
20
+
21
+ - Revised the package quickstart docstring, the README and the examples to lead
22
+ with the Inter-2 endpoints and an API key passed as ``access_token``. The
23
+ stream example selects ``api_version="v2"`` explicitly and forwards each
24
+ chunk its recorder (ffmpeg) writes, instead of cutting a file into fixed-size
25
+ slices. The ``auth.py`` example is removed. Documentation only; no behavior
26
+ change.
27
+
8
28
  ## 1.3.0
9
29
 
10
30
  - Added the whole-file signal list to upload jobs. ``submit(include=...)``
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: interhumanai
3
- Version: 1.3.0
3
+ Version: 1.3.2
4
4
  Summary: First-party Python SDK for the Interhuman API: upload and stream social-signal analysis.
5
5
  Project-URL: Homepage, https://docs.interhuman.ai
6
6
  Project-URL: Repository, https://github.com/interhumanai/interhuman-api
@@ -49,31 +49,43 @@ inherently event-driven, so a single async API keeps every surface consistent
49
49
 
50
50
  ```python
51
51
  import asyncio
52
- from interhumanai import InterhumanClient
52
+ import os
53
+
54
+ from interhumanai import InterhumanClient, UploadJobIncludeFlag, UploadJobStatus, UploadModel
53
55
 
54
56
  async def main() -> None:
55
- client = InterhumanClient(key_id="...", key_secret="...")
56
- result = await client.upload.analyze("meeting.mp4")
57
- for signal in result.signals:
58
- print(signal.type.value, signal.start, signal.end)
57
+ client = InterhumanClient(access_token=os.environ["INTERHUMAN_API_KEY"])
58
+ job = await client.upload.submit(
59
+ "meeting.mp4", model=UploadModel.INTER_2, include=[UploadJobIncludeFlag.SIGNALS]
60
+ )
61
+ job = await client.upload.wait_for_job(job)
62
+ if job.status is UploadJobStatus.COMPLETED:
63
+ for signal in job.result.signals:
64
+ print(signal.type.value, signal.start, signal.end)
65
+ else:
66
+ print(job.error.error_id, job.error.message)
59
67
 
60
68
  asyncio.run(main())
61
69
  ```
62
70
 
71
+ For a live feed, open `client.stream(api_version="v2")` (see [Stream API](#stream-api)).
72
+ Pass `api_version` explicitly: the default is `"v1"`.
73
+
63
74
  ## Authentication
64
75
 
65
- Two ways to authenticate:
76
+ Pass a credential as `access_token`; the client sends it as-is:
66
77
 
67
- - **API key credentials** (`key_id` + `key_secret`): the client authenticates
68
- with them and renews its session automatically.
69
- - **A client token** (see below) or your API key, passed as `access_token` and
70
- used as-is.
78
+ - **Your API key**, on your server.
79
+ - **A client token** (see below) on an untrusted client such as a browser,
80
+ which must never see the API key.
71
81
 
72
82
  ```python
73
- from interhumanai import InterhumanClient, Scope
83
+ import os
74
84
 
75
- # Managed credentials (recommended for servers)
76
- client = InterhumanClient(key_id="...", key_secret="...", scopes=[Scope.UPLOAD, Scope.STREAM])
85
+ from interhumanai import InterhumanClient
86
+
87
+ # On your server
88
+ client = InterhumanClient(access_token=os.environ["INTERHUMAN_API_KEY"])
77
89
 
78
90
  # A client token minted by your backend
79
91
  client = InterhumanClient(access_token=client_token)
@@ -86,13 +98,13 @@ custom credential source into the lower-level clients.
86
98
  Scopes name one endpoint family, on every API version that serves it:
87
99
 
88
100
  - `Scope.UPLOAD` (`interhumanai.upload`) — `POST /v1/upload/analyze`, and
89
- `POST /v2/upload/analyze` with `model="inter-2"`; also reads the account's
90
- jobs.
101
+ `POST /v2/upload/analyze` with every model it serves; also reads the
102
+ account's jobs.
91
103
  - `Scope.STREAM` (`interhumanai.stream`) — `WS /v1/stream/analyze`, and
92
- `WS /v2/stream/analyze` with `model="inter-2"`.
104
+ `WS /v2/stream/analyze` with every model it serves.
93
105
 
94
- Holding one scope never implies another, so request every scope the client
95
- needs.
106
+ A scope covers its endpoint on every API version and every model. Holding one
107
+ scope never implies another, so request every scope the client needs.
96
108
 
97
109
  ## Client tokens (browser / untrusted clients)
98
110
 
@@ -116,32 +128,10 @@ await auth.revoke_client_token(api_key="ih_...", token=token.access_token)
116
128
 
117
129
  ## Upload API
118
130
 
119
- Analyze a complete video file (mp4, avi, mov, mkv, mpeg-ts, or webm; at least
120
- 3 seconds, at most 32 MB):
121
-
122
- ```python
123
- from interhumanai import GoalDimension, IncludeFlag
124
-
125
- result = await client.upload.analyze(
126
- "meeting.mp4", # path, bytes, or file object
127
- include=[IncludeFlag.CONVERSATION_QUALITY_OVERALL], # optional sections
128
- goal_dimensions=[GoalDimension.CLARITY], # enables feedback
129
- )
130
- ```
131
-
132
- The typed `AnalysisResult` carries `signals`, `engagement_state`, and — when
133
- requested — `feedback` and `conversation_quality`.
134
-
135
- Every v1 `Signal` carries a `modality` naming the analysis modalities that
136
- detected it. When several tracks detect the same signal, every contributing
137
- modality is included, e.g. `["audio", "visual"]`. The Inter-2 (v2) routes send
138
- no `modality`: the model you choose names the evidence.
139
-
140
- ### Upload jobs with Inter-2 (`POST /v2/upload/analyze`)
141
-
142
- The v2 upload route analyzes a file with an Inter-2 model as an **asynchronous
143
- job**: the API validates the file, answers with a job envelope, and analyzes
144
- the file afterwards. Poll the job until it is `completed` or `failed`:
131
+ `client.upload.submit()` sends a file to `POST /v2/upload/analyze`, which
132
+ analyzes it with an Inter-2 model as an **asynchronous job**: the API validates
133
+ the file, answers with a job envelope, and analyzes the file afterwards. Poll
134
+ the job until it is `completed` or `failed`:
145
135
 
146
136
  ```python
147
137
  from interhumanai import UploadJobIncludeFlag, UploadModel
@@ -190,17 +180,50 @@ else:
190
180
  `UploadJobWindow` per fixed-length window, each with its span, an
191
181
  `engagement_status` and the `signals` read over it. Each signal is an
192
182
  `UploadJobSignal`: the v1 `Signal` without `modality`.
183
+ - `UploadModel.INTER_2_AUDIO` reads the audio alone: a wav, flac, mp3, m4a or
184
+ ogg file, or a webm or mp4 carrying an audio track, under the same limits.
185
+ It reports the same result shape, with a subset of the signals
186
+ `UploadModel.INTER_2` reports: agreement, confidence, frustration,
187
+ hesitation, interest and uncertainty. The same `Scope.UPLOAD` covers it.
188
+
189
+ ### Inter-1 upload (V1, compatibility)
190
+
191
+ `client.upload.analyze()` calls `POST /v1/upload/analyze`, which analyzes the
192
+ file with Inter-1 and answers synchronously. Use it only to keep an existing
193
+ V1 integration running; see
194
+ [Migrate from V1 to V2](https://docs.interhuman.ai/api-reference/migrate-v1-to-v2).
195
+ It takes a complete video file (mp4, avi, mov, mkv, mpeg-ts, or webm; at least
196
+ 3 seconds, at most 32 MB):
197
+
198
+ ```python
199
+ from interhumanai import GoalDimension, IncludeFlag
200
+
201
+ result = await client.upload.analyze(
202
+ "meeting.mp4", # path, bytes, or file object
203
+ include=[IncludeFlag.CONVERSATION_QUALITY_OVERALL], # optional sections
204
+ goal_dimensions=[GoalDimension.CLARITY], # enables feedback
205
+ )
206
+ ```
207
+
208
+ The typed `AnalysisResult` carries `signals`, `engagement_state`, and — when
209
+ requested — `feedback` and `conversation_quality`.
210
+
211
+ Every v1 `Signal` carries a `modality` naming the analysis modalities that
212
+ detected it. When several tracks detect the same signal, every contributing
213
+ modality is included, e.g. `["audio", "visual"]`. The Inter-2 (v2) routes send
214
+ no `modality`: the model you choose names the evidence.
193
215
 
194
216
  ## Stream API
195
217
 
196
- One `StreamClient` handles one live session against `WS /v1/stream/analyze`.
197
- Send binary WebM or fragmented-MP4 chunks and consume typed events with
198
- `async for`:
218
+ One `StreamClient` handles one live session. Pass `api_version="v2"` to open
219
+ `WS /v2/stream/analyze`, analyzed by Inter-2. Send binary WebM or
220
+ fragmented-MP4 chunks as your recorder produces them, and consume typed events
221
+ with `async for`:
199
222
 
200
223
  ```python
201
224
  from interhumanai import IncludeFlag, SignalDetectedEvent
202
225
 
203
- async with client.stream() as session:
226
+ async with client.stream(api_version="v2") as session:
204
227
  await session.wait_for_session_ready()
205
228
  await session.update_config(include=[IncludeFlag.CONVERSATION_QUALITY_OVERALL])
206
229
  await session.send_video(first_chunk) # first chunk carries the container header
@@ -216,31 +239,26 @@ close code and reason. Event types this SDK version does not know arrive as
216
239
 
217
240
  ### Choosing the model: v1 and v2
218
241
 
219
- `client.stream()` opens `WS /v1/stream/analyze`, analyzed by the Inter-1
220
- model. Pass `api_version="v2"` to open `WS /v2/stream/analyze` and have the
221
- same session analyzed by the Inter-2 model:
222
-
223
- ```python
224
- async with client.stream(api_version="v2") as session:
225
- ...
226
- ```
227
-
228
- Everything else is identical — the `session.ready` limits, session config, the
229
- video you send, the events you receive and their order, the same `Scope.STREAM`
230
- scope for the default model, and the graceful close — so the code above works
231
- unchanged, except that
232
- v2 signal events carry no `modality` (`event.data.modality` is `None`). One
233
- more thing differs: how the client names itself: errors raised for a v2 session say "Inter-2
234
- stream" where a v1 session says "Stream", so a message names the endpoint the
235
- session actually opened. If a deployment has no Inter-2 backend configured, a
236
- v2 session is refused right after the handshake with an `ErrorEvent`
237
- (`ih1003`) and close code 1013.
242
+ `client.stream()` without `api_version` opens `WS /v1/stream/analyze`,
243
+ analyzed by the Inter-1 model, so always pass `api_version="v2"` for Inter-2.
244
+ Use V1 only to keep an existing integration running.
245
+
246
+ Apart from the endpoint and the model, V1 and V2 sessions are identical: the
247
+ `session.ready` limits, session config, the video you send, the events you
248
+ receive and their order, the same `Scope.STREAM` scope for every model, and
249
+ the graceful close. Code written for V1 therefore works on V2 unchanged,
250
+ except that v2 signal events carry no `modality` (`event.data.modality` is
251
+ `None`). Errors raised for a v2 session also say "Inter-2 stream" where a v1
252
+ session says "Stream", so a message names the endpoint the session actually
253
+ opened. If a deployment has no Inter-2 backend configured, a v2 session is
254
+ refused right after the handshake with an `ErrorEvent` (`ih1003`) and close
255
+ code 1013.
238
256
 
239
257
  ### Choosing the Inter-2 model
240
258
 
241
259
  On `api_version="v2"` the `model` argument names the model the session opens
242
- on. It defaults to `StreamModel.INTER_2`, which reads the video and which
243
- `Scope.STREAM` covers. `session.ready` lists the models the deployment serves
260
+ on. It defaults to `StreamModel.INTER_2`, which reads the video;
261
+ `Scope.STREAM` covers every model. `session.ready` lists the models the deployment serves
244
262
  that your credential may select under `supported_session_config_options.model`,
245
263
  and `session.updated` echoes the one in force.
246
264
 
@@ -251,6 +269,23 @@ async with client.stream(api_version="v2", model=StreamModel.INTER_2) as session
251
269
  await session.wait_for_session_ready()
252
270
  ```
253
271
 
272
+ `StreamModel.INTER_2_AUDIO` hears the audio alone and reports a subset of the
273
+ signals `StreamModel.INTER_2` reports (agreement, confidence, frustration,
274
+ hesitation, interest, uncertainty). It takes the same WebM or fragmented-MP4
275
+ clips through `send_video`, with or without a video track, so an audio-only
276
+ recording works as-is. To send raw audio instead, declare the format once in
277
+ the session config and then send samples with `send_audio`: signed 16-bit
278
+ little-endian mono PCM at 16000 or 24000 Hz, cut at any byte boundary.
279
+
280
+ ```python
281
+ from interhumanai import RawAudioFormat, StreamModel
282
+
283
+ async with client.stream(api_version="v2", model=StreamModel.INTER_2_AUDIO) as session:
284
+ await session.wait_for_session_ready()
285
+ await session.update_config(audio_format=RawAudioFormat(sample_rate=16000))
286
+ await session.send_audio(pcm_frame)
287
+ ```
288
+
254
289
  ## Errors
255
290
 
256
291
  Three surfaces, mirroring the API:
@@ -294,17 +329,18 @@ want to inspect exactly what is sent.
294
329
  ## Environments
295
330
 
296
331
  ```python
297
- InterhumanClient(key_id=..., key_secret=...) # production (default): api.interhuman.ai
298
- InterhumanClient(key_id=..., key_secret=..., environment="staging") # staging-api.interhuman.ai
299
- InterhumanClient(key_id=..., key_secret=..., base_url="http://localhost:8080") # local override
332
+ InterhumanClient(access_token=api_key) # production (default): api.interhuman.ai
333
+ InterhumanClient(access_token=api_key, environment="staging") # staging-api.interhuman.ai
334
+ InterhumanClient(access_token=api_key, base_url="http://localhost:8080") # local override
300
335
  ```
301
336
 
302
337
  WebSocket URLs are derived automatically (`https://` → `wss://`).
303
338
 
304
339
  ## Examples
305
340
 
306
- Runnable scripts for every flow live in [`examples/`](examples/): `auth.py`,
307
- `client_tokens.py`, `upload.py`, `upload_job.py`, and `stream.py`.
341
+ Runnable scripts for every flow live in [`examples/`](examples/):
342
+ `upload_job.py` (Inter-2 upload), `stream.py` (Inter-2 stream),
343
+ `client_tokens.py`, and `upload.py` (V1 compatibility).
308
344
 
309
345
  ## Development
310
346
 
@@ -23,31 +23,43 @@ inherently event-driven, so a single async API keeps every surface consistent
23
23
 
24
24
  ```python
25
25
  import asyncio
26
- from interhumanai import InterhumanClient
26
+ import os
27
+
28
+ from interhumanai import InterhumanClient, UploadJobIncludeFlag, UploadJobStatus, UploadModel
27
29
 
28
30
  async def main() -> None:
29
- client = InterhumanClient(key_id="...", key_secret="...")
30
- result = await client.upload.analyze("meeting.mp4")
31
- for signal in result.signals:
32
- print(signal.type.value, signal.start, signal.end)
31
+ client = InterhumanClient(access_token=os.environ["INTERHUMAN_API_KEY"])
32
+ job = await client.upload.submit(
33
+ "meeting.mp4", model=UploadModel.INTER_2, include=[UploadJobIncludeFlag.SIGNALS]
34
+ )
35
+ job = await client.upload.wait_for_job(job)
36
+ if job.status is UploadJobStatus.COMPLETED:
37
+ for signal in job.result.signals:
38
+ print(signal.type.value, signal.start, signal.end)
39
+ else:
40
+ print(job.error.error_id, job.error.message)
33
41
 
34
42
  asyncio.run(main())
35
43
  ```
36
44
 
45
+ For a live feed, open `client.stream(api_version="v2")` (see [Stream API](#stream-api)).
46
+ Pass `api_version` explicitly: the default is `"v1"`.
47
+
37
48
  ## Authentication
38
49
 
39
- Two ways to authenticate:
50
+ Pass a credential as `access_token`; the client sends it as-is:
40
51
 
41
- - **API key credentials** (`key_id` + `key_secret`): the client authenticates
42
- with them and renews its session automatically.
43
- - **A client token** (see below) or your API key, passed as `access_token` and
44
- used as-is.
52
+ - **Your API key**, on your server.
53
+ - **A client token** (see below) on an untrusted client such as a browser,
54
+ which must never see the API key.
45
55
 
46
56
  ```python
47
- from interhumanai import InterhumanClient, Scope
57
+ import os
48
58
 
49
- # Managed credentials (recommended for servers)
50
- client = InterhumanClient(key_id="...", key_secret="...", scopes=[Scope.UPLOAD, Scope.STREAM])
59
+ from interhumanai import InterhumanClient
60
+
61
+ # On your server
62
+ client = InterhumanClient(access_token=os.environ["INTERHUMAN_API_KEY"])
51
63
 
52
64
  # A client token minted by your backend
53
65
  client = InterhumanClient(access_token=client_token)
@@ -60,13 +72,13 @@ custom credential source into the lower-level clients.
60
72
  Scopes name one endpoint family, on every API version that serves it:
61
73
 
62
74
  - `Scope.UPLOAD` (`interhumanai.upload`) — `POST /v1/upload/analyze`, and
63
- `POST /v2/upload/analyze` with `model="inter-2"`; also reads the account's
64
- jobs.
75
+ `POST /v2/upload/analyze` with every model it serves; also reads the
76
+ account's jobs.
65
77
  - `Scope.STREAM` (`interhumanai.stream`) — `WS /v1/stream/analyze`, and
66
- `WS /v2/stream/analyze` with `model="inter-2"`.
78
+ `WS /v2/stream/analyze` with every model it serves.
67
79
 
68
- Holding one scope never implies another, so request every scope the client
69
- needs.
80
+ A scope covers its endpoint on every API version and every model. Holding one
81
+ scope never implies another, so request every scope the client needs.
70
82
 
71
83
  ## Client tokens (browser / untrusted clients)
72
84
 
@@ -90,32 +102,10 @@ await auth.revoke_client_token(api_key="ih_...", token=token.access_token)
90
102
 
91
103
  ## Upload API
92
104
 
93
- Analyze a complete video file (mp4, avi, mov, mkv, mpeg-ts, or webm; at least
94
- 3 seconds, at most 32 MB):
95
-
96
- ```python
97
- from interhumanai import GoalDimension, IncludeFlag
98
-
99
- result = await client.upload.analyze(
100
- "meeting.mp4", # path, bytes, or file object
101
- include=[IncludeFlag.CONVERSATION_QUALITY_OVERALL], # optional sections
102
- goal_dimensions=[GoalDimension.CLARITY], # enables feedback
103
- )
104
- ```
105
-
106
- The typed `AnalysisResult` carries `signals`, `engagement_state`, and — when
107
- requested — `feedback` and `conversation_quality`.
108
-
109
- Every v1 `Signal` carries a `modality` naming the analysis modalities that
110
- detected it. When several tracks detect the same signal, every contributing
111
- modality is included, e.g. `["audio", "visual"]`. The Inter-2 (v2) routes send
112
- no `modality`: the model you choose names the evidence.
113
-
114
- ### Upload jobs with Inter-2 (`POST /v2/upload/analyze`)
115
-
116
- The v2 upload route analyzes a file with an Inter-2 model as an **asynchronous
117
- job**: the API validates the file, answers with a job envelope, and analyzes
118
- the file afterwards. Poll the job until it is `completed` or `failed`:
105
+ `client.upload.submit()` sends a file to `POST /v2/upload/analyze`, which
106
+ analyzes it with an Inter-2 model as an **asynchronous job**: the API validates
107
+ the file, answers with a job envelope, and analyzes the file afterwards. Poll
108
+ the job until it is `completed` or `failed`:
119
109
 
120
110
  ```python
121
111
  from interhumanai import UploadJobIncludeFlag, UploadModel
@@ -164,17 +154,50 @@ else:
164
154
  `UploadJobWindow` per fixed-length window, each with its span, an
165
155
  `engagement_status` and the `signals` read over it. Each signal is an
166
156
  `UploadJobSignal`: the v1 `Signal` without `modality`.
157
+ - `UploadModel.INTER_2_AUDIO` reads the audio alone: a wav, flac, mp3, m4a or
158
+ ogg file, or a webm or mp4 carrying an audio track, under the same limits.
159
+ It reports the same result shape, with a subset of the signals
160
+ `UploadModel.INTER_2` reports: agreement, confidence, frustration,
161
+ hesitation, interest and uncertainty. The same `Scope.UPLOAD` covers it.
162
+
163
+ ### Inter-1 upload (V1, compatibility)
164
+
165
+ `client.upload.analyze()` calls `POST /v1/upload/analyze`, which analyzes the
166
+ file with Inter-1 and answers synchronously. Use it only to keep an existing
167
+ V1 integration running; see
168
+ [Migrate from V1 to V2](https://docs.interhuman.ai/api-reference/migrate-v1-to-v2).
169
+ It takes a complete video file (mp4, avi, mov, mkv, mpeg-ts, or webm; at least
170
+ 3 seconds, at most 32 MB):
171
+
172
+ ```python
173
+ from interhumanai import GoalDimension, IncludeFlag
174
+
175
+ result = await client.upload.analyze(
176
+ "meeting.mp4", # path, bytes, or file object
177
+ include=[IncludeFlag.CONVERSATION_QUALITY_OVERALL], # optional sections
178
+ goal_dimensions=[GoalDimension.CLARITY], # enables feedback
179
+ )
180
+ ```
181
+
182
+ The typed `AnalysisResult` carries `signals`, `engagement_state`, and — when
183
+ requested — `feedback` and `conversation_quality`.
184
+
185
+ Every v1 `Signal` carries a `modality` naming the analysis modalities that
186
+ detected it. When several tracks detect the same signal, every contributing
187
+ modality is included, e.g. `["audio", "visual"]`. The Inter-2 (v2) routes send
188
+ no `modality`: the model you choose names the evidence.
167
189
 
168
190
  ## Stream API
169
191
 
170
- One `StreamClient` handles one live session against `WS /v1/stream/analyze`.
171
- Send binary WebM or fragmented-MP4 chunks and consume typed events with
172
- `async for`:
192
+ One `StreamClient` handles one live session. Pass `api_version="v2"` to open
193
+ `WS /v2/stream/analyze`, analyzed by Inter-2. Send binary WebM or
194
+ fragmented-MP4 chunks as your recorder produces them, and consume typed events
195
+ with `async for`:
173
196
 
174
197
  ```python
175
198
  from interhumanai import IncludeFlag, SignalDetectedEvent
176
199
 
177
- async with client.stream() as session:
200
+ async with client.stream(api_version="v2") as session:
178
201
  await session.wait_for_session_ready()
179
202
  await session.update_config(include=[IncludeFlag.CONVERSATION_QUALITY_OVERALL])
180
203
  await session.send_video(first_chunk) # first chunk carries the container header
@@ -190,31 +213,26 @@ close code and reason. Event types this SDK version does not know arrive as
190
213
 
191
214
  ### Choosing the model: v1 and v2
192
215
 
193
- `client.stream()` opens `WS /v1/stream/analyze`, analyzed by the Inter-1
194
- model. Pass `api_version="v2"` to open `WS /v2/stream/analyze` and have the
195
- same session analyzed by the Inter-2 model:
196
-
197
- ```python
198
- async with client.stream(api_version="v2") as session:
199
- ...
200
- ```
201
-
202
- Everything else is identical — the `session.ready` limits, session config, the
203
- video you send, the events you receive and their order, the same `Scope.STREAM`
204
- scope for the default model, and the graceful close — so the code above works
205
- unchanged, except that
206
- v2 signal events carry no `modality` (`event.data.modality` is `None`). One
207
- more thing differs: how the client names itself: errors raised for a v2 session say "Inter-2
208
- stream" where a v1 session says "Stream", so a message names the endpoint the
209
- session actually opened. If a deployment has no Inter-2 backend configured, a
210
- v2 session is refused right after the handshake with an `ErrorEvent`
211
- (`ih1003`) and close code 1013.
216
+ `client.stream()` without `api_version` opens `WS /v1/stream/analyze`,
217
+ analyzed by the Inter-1 model, so always pass `api_version="v2"` for Inter-2.
218
+ Use V1 only to keep an existing integration running.
219
+
220
+ Apart from the endpoint and the model, V1 and V2 sessions are identical: the
221
+ `session.ready` limits, session config, the video you send, the events you
222
+ receive and their order, the same `Scope.STREAM` scope for every model, and
223
+ the graceful close. Code written for V1 therefore works on V2 unchanged,
224
+ except that v2 signal events carry no `modality` (`event.data.modality` is
225
+ `None`). Errors raised for a v2 session also say "Inter-2 stream" where a v1
226
+ session says "Stream", so a message names the endpoint the session actually
227
+ opened. If a deployment has no Inter-2 backend configured, a v2 session is
228
+ refused right after the handshake with an `ErrorEvent` (`ih1003`) and close
229
+ code 1013.
212
230
 
213
231
  ### Choosing the Inter-2 model
214
232
 
215
233
  On `api_version="v2"` the `model` argument names the model the session opens
216
- on. It defaults to `StreamModel.INTER_2`, which reads the video and which
217
- `Scope.STREAM` covers. `session.ready` lists the models the deployment serves
234
+ on. It defaults to `StreamModel.INTER_2`, which reads the video;
235
+ `Scope.STREAM` covers every model. `session.ready` lists the models the deployment serves
218
236
  that your credential may select under `supported_session_config_options.model`,
219
237
  and `session.updated` echoes the one in force.
220
238
 
@@ -225,6 +243,23 @@ async with client.stream(api_version="v2", model=StreamModel.INTER_2) as session
225
243
  await session.wait_for_session_ready()
226
244
  ```
227
245
 
246
+ `StreamModel.INTER_2_AUDIO` hears the audio alone and reports a subset of the
247
+ signals `StreamModel.INTER_2` reports (agreement, confidence, frustration,
248
+ hesitation, interest, uncertainty). It takes the same WebM or fragmented-MP4
249
+ clips through `send_video`, with or without a video track, so an audio-only
250
+ recording works as-is. To send raw audio instead, declare the format once in
251
+ the session config and then send samples with `send_audio`: signed 16-bit
252
+ little-endian mono PCM at 16000 or 24000 Hz, cut at any byte boundary.
253
+
254
+ ```python
255
+ from interhumanai import RawAudioFormat, StreamModel
256
+
257
+ async with client.stream(api_version="v2", model=StreamModel.INTER_2_AUDIO) as session:
258
+ await session.wait_for_session_ready()
259
+ await session.update_config(audio_format=RawAudioFormat(sample_rate=16000))
260
+ await session.send_audio(pcm_frame)
261
+ ```
262
+
228
263
  ## Errors
229
264
 
230
265
  Three surfaces, mirroring the API:
@@ -268,17 +303,18 @@ want to inspect exactly what is sent.
268
303
  ## Environments
269
304
 
270
305
  ```python
271
- InterhumanClient(key_id=..., key_secret=...) # production (default): api.interhuman.ai
272
- InterhumanClient(key_id=..., key_secret=..., environment="staging") # staging-api.interhuman.ai
273
- InterhumanClient(key_id=..., key_secret=..., base_url="http://localhost:8080") # local override
306
+ InterhumanClient(access_token=api_key) # production (default): api.interhuman.ai
307
+ InterhumanClient(access_token=api_key, environment="staging") # staging-api.interhuman.ai
308
+ InterhumanClient(access_token=api_key, base_url="http://localhost:8080") # local override
274
309
  ```
275
310
 
276
311
  WebSocket URLs are derived automatically (`https://` → `wss://`).
277
312
 
278
313
  ## Examples
279
314
 
280
- Runnable scripts for every flow live in [`examples/`](examples/): `auth.py`,
281
- `client_tokens.py`, `upload.py`, `upload_job.py`, and `stream.py`.
315
+ Runnable scripts for every flow live in [`examples/`](examples/):
316
+ `upload_job.py` (Inter-2 upload), `stream.py` (Inter-2 stream),
317
+ `client_tokens.py`, and `upload.py` (V1 compatibility).
282
318
 
283
319
  ## Development
284
320
 
@@ -3,15 +3,27 @@
3
3
  Quickstart::
4
4
 
5
5
  import asyncio
6
- from interhumanai import InterhumanClient
6
+ import os
7
+
8
+ from interhumanai import InterhumanClient, UploadJobIncludeFlag, UploadJobStatus, UploadModel
7
9
 
8
10
  async def main() -> None:
9
- client = InterhumanClient(key_id="...", key_secret="...")
10
- result = await client.upload.analyze("meeting.mp4")
11
- for signal in result.signals:
12
- print(signal.type.value, signal.start, signal.end)
11
+ client = InterhumanClient(access_token=os.environ["INTERHUMAN_API_KEY"])
12
+ job = await client.upload.submit(
13
+ "meeting.mp4", model=UploadModel.INTER_2, include=[UploadJobIncludeFlag.SIGNALS]
14
+ )
15
+ job = await client.upload.wait_for_job(job)
16
+ if job.status is UploadJobStatus.COMPLETED:
17
+ for signal in job.result.signals:
18
+ print(signal.type.value, signal.start, signal.end)
19
+ else:
20
+ print(job.error.error_id, job.error.message)
13
21
 
14
22
  asyncio.run(main())
23
+
24
+ For a live feed, ``client.stream(api_version="v2")`` opens
25
+ ``WS /v2/stream/analyze``. Pass ``api_version`` explicitly: the default is
26
+ ``"v1"``.
15
27
  """
16
28
 
17
29
  from ._sdk_metadata import SDK_HEADER_NAME, SDK_NAME, sdk_header_value
@@ -1,3 +1,3 @@
1
1
  """Package version (single source of truth, read by hatchling at build time)."""
2
2
 
3
- __version__ = "1.3.0"
3
+ __version__ = "1.3.2"
@@ -23,10 +23,10 @@ from .upload import UploadClient
23
23
  class InterhumanClient:
24
24
  """High-level client for the Interhuman API.
25
25
 
26
- Authenticate either with API key credentials (``key_id`` + ``key_secret``,
27
- exchanged for short-lived bearer tokens that are refreshed automatically)
28
- or with a pre-issued ``access_token`` used as-is. Exactly one of the two
29
- must be provided.
26
+ Authenticate with ``access_token``: your API key, or a client token your
27
+ server minted, used as-is. Alternatively pass API key credentials
28
+ (``key_id`` + ``key_secret``), exchanged for short-lived bearer tokens that
29
+ are refreshed automatically. Exactly one of the two must be provided.
30
30
 
31
31
  The client exposes every public surface: :attr:`auth` for token endpoints,
32
32
  :attr:`upload` for complete files, and :meth:`stream` for live WebSocket
@@ -35,7 +35,7 @@ class InterhumanClient:
35
35
  Args:
36
36
  key_id: API key id (paired with ``key_secret``).
37
37
  key_secret: API key secret.
38
- access_token: A client token, or your API key, used as-is.
38
+ access_token: Your API key, or a client token, used as-is.
39
39
  scopes: Scopes requested when exchanging credentials. Defaults to
40
40
  upload + stream. Ignored when ``access_token`` is used.
41
41
  environment: Named environment to call. Defaults to ``production``.
@@ -18,7 +18,6 @@ from .types import (
18
18
  IncludeFlag,
19
19
  Probability,
20
20
  RawAudioFormat,
21
- Scope,
22
21
  SignalType,
23
22
  StreamModel,
24
23
  )
@@ -403,7 +402,7 @@ class StreamClient(SessionSocketClient[StreamEvent]):
403
402
 
404
403
  ``api_version`` selects the endpoint: ``"v1"`` (the default) is analyzed by
405
404
  the Inter-1 model; ``"v2"`` is analyzed by an Inter-2 model. Both require
406
- the ``interhumanai.stream`` scope for the default model. The session
405
+ the ``interhumanai.stream`` scope, which covers every model. The session
407
406
  protocol and the events are identical on both, and each names the endpoint
408
407
  it actually opened in the errors it raises: a v2 session reports the
409
408
  Inter-2 stream, a v1 session the Stream.
@@ -462,14 +461,9 @@ class StreamClient(SessionSocketClient[StreamEvent]):
462
461
  self._model = model_value
463
462
  if model_value is not None:
464
463
  self._handshake_query["model"] = model_value
465
- # The audio model is authorized by its own scope, so the hint names it.
466
- if api_version == "v2" and model_value == StreamModel.INTER_2_AUDIO.value:
467
- self._scope_hint = (
468
- "The credentials may be invalid or lack the "
469
- f"{Scope.STREAM_INTER_2_AUDIO.value} scope."
470
- )
471
- else:
472
- self._scope_hint = "The credentials may be invalid or lack the stream scope."
464
+ # One stream scope covers both endpoints and every model, so the hint
465
+ # never names a model.
466
+ self._scope_hint = "The credentials may be invalid or lack the stream scope."
473
467
 
474
468
  @property
475
469
  def api_version(self) -> StreamApiVersion:
@@ -512,9 +506,14 @@ class StreamClient(SessionSocketClient[StreamEvent]):
512
506
  goal_dimensions: Goal dimensions that enable feedback generation.
513
507
  model: The Inter-2 model to analyze the session with, on
514
508
  ``api_version="v2"`` only. ``StreamModel.INTER_2`` (the
515
- default) reads the video.
516
- audio_format: Session state like ``model``. Not used by
517
- ``StreamModel.INTER_2`` sessions; leave it unset.
509
+ default) reads the video; ``StreamModel.INTER_2_AUDIO`` hears
510
+ the audio alone.
511
+ audio_format: Declares that the session's binary frames are raw
512
+ PCM audio rather than container media, on
513
+ ``StreamModel.INTER_2_AUDIO`` sessions only. Session state
514
+ like ``model``: omitted keeps the format in force, and it is
515
+ fixed once the first audio frame has been accepted. Leave it
516
+ unset to send WebM or MP4 clips.
518
517
  """
519
518
  config: dict[str, Any] = {}
520
519
  if include is not None:
@@ -534,9 +533,10 @@ class StreamClient(SessionSocketClient[StreamEvent]):
534
533
  async def send_audio(self, frame: bytes | bytearray | memoryview) -> None:
535
534
  """Send one frame of raw PCM audio.
536
535
 
537
- Only meaningful on ``api_version="v2"`` after :meth:`update_config`
538
- declared an ``audio_format`` on a model that accepts raw audio; the
539
- server then reads every binary frame as signed 16-bit mono PCM at the
536
+ Only meaningful on ``api_version="v2"`` with
537
+ ``model=StreamModel.INTER_2_AUDIO``, after :meth:`update_config`
538
+ declared an ``audio_format``; the server then reads every binary frame
539
+ as signed 16-bit mono PCM at the
540
540
  declared sample rate. Frames can be any size and cut at any byte
541
541
  boundary. Without that declaration a binary frame is container media,
542
542
  which :meth:`send_video` names.
@@ -12,20 +12,23 @@ class Scope(str, enum.Enum):
12
12
 
13
13
  A scope names one endpoint family across every API version. ``UPLOAD``
14
14
  covers ``POST /v1/upload/analyze`` and ``POST /v2/upload/analyze`` with
15
- ``model="inter-2"``, and reads the account's jobs at
15
+ every model it serves, and reads the account's jobs at
16
16
  ``GET /v2/upload/jobs/{job_id}``; ``STREAM`` covers ``WS /v1/stream/analyze``
17
- and ``WS /v2/stream/analyze`` with ``model="inter-2"``. No scope implies
17
+ and ``WS /v2/stream/analyze`` with every model it serves. No scope implies
18
18
  another. The token response's ``scope`` is authoritative.
19
19
  """
20
20
 
21
21
  UPLOAD = "interhumanai.upload"
22
22
  STREAM = "interhumanai.stream"
23
23
  REALTIME = "interhumanai.realtime"
24
- UPLOAD_INTER_2_AUDIO = "interhumanai.upload.inter-2-audio"
25
24
  UPLOAD_INTER_2_DEEP = "interhumanai.upload.inter-2-deep"
26
- STREAM_INTER_2_AUDIO = "interhumanai.stream.inter-2-audio"
25
+ # Legacy names of UPLOAD and STREAM, accepted during the cooling-off
26
+ # period and issued as the endpoint scope. The two audio members were
27
+ # program scopes before the audio model moved under the endpoint scopes.
27
28
  UPLOAD_INTER_2 = "interhumanai.upload.inter-2"
29
+ UPLOAD_INTER_2_AUDIO = "interhumanai.upload.inter-2-audio"
28
30
  STREAM_INTER_2 = "interhumanai.stream.inter-2"
31
+ STREAM_INTER_2_AUDIO = "interhumanai.stream.inter-2-audio"
29
32
 
30
33
 
31
34
  class SignalType(str, enum.Enum):
@@ -185,8 +188,13 @@ class AnalysisResult(BaseModel):
185
188
  class StreamModel(str, enum.Enum):
186
189
  """Inter-2 models a ``WS /v2/stream/analyze`` session can select.
187
190
 
188
- ``INTER_2`` (the default) reads the video. ``WS /v1/stream/analyze``
189
- offers no selection and rejects any value.
191
+ ``INTER_2`` (the default) reads the video. ``INTER_2_AUDIO`` hears the
192
+ audio alone: it accepts the same WebM and fragmented MP4 clips, with or
193
+ without a video track, and raw PCM frames once the session config declares
194
+ an :class:`RawAudioFormat`, and reports a subset of the signals
195
+ ``INTER_2`` reports (agreement, confidence, frustration, hesitation,
196
+ interest, uncertainty). ``WS /v1/stream/analyze`` offers no selection and
197
+ rejects any value.
190
198
  """
191
199
 
192
200
  INTER_2 = "inter-2"
@@ -202,7 +210,8 @@ class RawAudioFormat(BaseModel):
202
210
  """Declares that a session's binary frames are raw audio, not container media.
203
211
 
204
212
  Signed 16-bit little-endian PCM at ``sample_rate``, mono, with no header;
205
- frames may be cut at any byte boundary.
213
+ frames may be cut at any byte boundary. Accepted on
214
+ ``StreamModel.INTER_2_AUDIO`` sessions only.
206
215
 
207
216
  Attributes:
208
217
  encoding: Always ``pcm_s16le``.
@@ -218,7 +227,12 @@ class RawAudioFormat(BaseModel):
218
227
  class UploadModel(str, enum.Enum):
219
228
  """Inter-2 models a ``POST /v2/upload/analyze`` job can name.
220
229
 
221
- ``INTER_2`` reads a video (picture and sound together).
230
+ ``INTER_2`` reads a video (picture and sound together): an mp4, mov, avi,
231
+ mkv, webm or mpeg-ts file carrying both a video and an audio track.
232
+ ``INTER_2_AUDIO`` reads the audio alone: a wav, flac, mp3, m4a or ogg
233
+ file, or a webm or mp4 carrying an audio track. It reports a subset of the
234
+ signals ``INTER_2`` reports (agreement, confidence, frustration,
235
+ hesitation, interest, uncertainty).
222
236
  """
223
237
 
224
238
  INTER_2 = "inter-2"
File without changes
File without changes