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.
- {interhumanai-1.3.0 → interhumanai-1.3.2}/CHANGELOG.md +20 -0
- {interhumanai-1.3.0 → interhumanai-1.3.2}/PKG-INFO +111 -75
- {interhumanai-1.3.0 → interhumanai-1.3.2}/README.md +110 -74
- {interhumanai-1.3.0 → interhumanai-1.3.2}/src/interhumanai/__init__.py +17 -5
- {interhumanai-1.3.0 → interhumanai-1.3.2}/src/interhumanai/_version.py +1 -1
- {interhumanai-1.3.0 → interhumanai-1.3.2}/src/interhumanai/client.py +5 -5
- {interhumanai-1.3.0 → interhumanai-1.3.2}/src/interhumanai/stream.py +16 -16
- {interhumanai-1.3.0 → interhumanai-1.3.2}/src/interhumanai/types.py +22 -8
- {interhumanai-1.3.0 → interhumanai-1.3.2}/.gitignore +0 -0
- {interhumanai-1.3.0 → interhumanai-1.3.2}/LICENSE +0 -0
- {interhumanai-1.3.0 → interhumanai-1.3.2}/pyproject.toml +0 -0
- {interhumanai-1.3.0 → interhumanai-1.3.2}/src/interhumanai/_http.py +0 -0
- {interhumanai-1.3.0 → interhumanai-1.3.2}/src/interhumanai/_sdk_metadata.py +0 -0
- {interhumanai-1.3.0 → interhumanai-1.3.2}/src/interhumanai/_session_socket.py +0 -0
- {interhumanai-1.3.0 → interhumanai-1.3.2}/src/interhumanai/auth.py +0 -0
- {interhumanai-1.3.0 → interhumanai-1.3.2}/src/interhumanai/config.py +0 -0
- {interhumanai-1.3.0 → interhumanai-1.3.2}/src/interhumanai/errors.py +0 -0
- {interhumanai-1.3.0 → interhumanai-1.3.2}/src/interhumanai/py.typed +0 -0
- {interhumanai-1.3.0 → interhumanai-1.3.2}/src/interhumanai/realtime.py +0 -0
- {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.
|
|
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
|
-
|
|
52
|
+
import os
|
|
53
|
+
|
|
54
|
+
from interhumanai import InterhumanClient, UploadJobIncludeFlag, UploadJobStatus, UploadModel
|
|
53
55
|
|
|
54
56
|
async def main() -> None:
|
|
55
|
-
client = InterhumanClient(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
76
|
+
Pass a credential as `access_token`; the client sends it as-is:
|
|
66
77
|
|
|
67
|
-
- **API key
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
83
|
+
import os
|
|
74
84
|
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
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
|
|
104
|
+
`WS /v2/stream/analyze` with every model it serves.
|
|
93
105
|
|
|
94
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
|
197
|
-
|
|
198
|
-
|
|
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`,
|
|
220
|
-
model
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
|
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(
|
|
298
|
-
InterhumanClient(
|
|
299
|
-
InterhumanClient(
|
|
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/):
|
|
307
|
-
`
|
|
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
|
-
|
|
26
|
+
import os
|
|
27
|
+
|
|
28
|
+
from interhumanai import InterhumanClient, UploadJobIncludeFlag, UploadJobStatus, UploadModel
|
|
27
29
|
|
|
28
30
|
async def main() -> None:
|
|
29
|
-
client = InterhumanClient(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
50
|
+
Pass a credential as `access_token`; the client sends it as-is:
|
|
40
51
|
|
|
41
|
-
- **API key
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
57
|
+
import os
|
|
48
58
|
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
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
|
|
78
|
+
`WS /v2/stream/analyze` with every model it serves.
|
|
67
79
|
|
|
68
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
171
|
-
|
|
172
|
-
|
|
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`,
|
|
194
|
-
model
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
|
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(
|
|
272
|
-
InterhumanClient(
|
|
273
|
-
InterhumanClient(
|
|
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/):
|
|
281
|
-
`
|
|
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
|
-
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
from interhumanai import InterhumanClient, UploadJobIncludeFlag, UploadJobStatus, UploadModel
|
|
7
9
|
|
|
8
10
|
async def main() -> None:
|
|
9
|
-
client = InterhumanClient(
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
@@ -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
|
|
27
|
-
|
|
28
|
-
|
|
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:
|
|
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
|
|
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
|
-
#
|
|
466
|
-
|
|
467
|
-
|
|
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
|
-
|
|
517
|
-
|
|
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"``
|
|
538
|
-
|
|
539
|
-
server then reads every binary frame
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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. ``
|
|
189
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|