interhumanai 1.2.3__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.2.3 → interhumanai-1.3.2}/CHANGELOG.md +31 -0
- {interhumanai-1.2.3 → interhumanai-1.3.2}/PKG-INFO +125 -82
- {interhumanai-1.2.3 → interhumanai-1.3.2}/README.md +124 -81
- {interhumanai-1.2.3 → interhumanai-1.3.2}/src/interhumanai/__init__.py +19 -5
- {interhumanai-1.2.3 → interhumanai-1.3.2}/src/interhumanai/_version.py +1 -1
- {interhumanai-1.2.3 → interhumanai-1.3.2}/src/interhumanai/client.py +5 -5
- {interhumanai-1.2.3 → interhumanai-1.3.2}/src/interhumanai/stream.py +16 -16
- {interhumanai-1.2.3 → interhumanai-1.3.2}/src/interhumanai/types.py +46 -9
- {interhumanai-1.2.3 → interhumanai-1.3.2}/src/interhumanai/upload.py +23 -11
- {interhumanai-1.2.3 → interhumanai-1.3.2}/.gitignore +0 -0
- {interhumanai-1.2.3 → interhumanai-1.3.2}/LICENSE +0 -0
- {interhumanai-1.2.3 → interhumanai-1.3.2}/pyproject.toml +0 -0
- {interhumanai-1.2.3 → interhumanai-1.3.2}/src/interhumanai/_http.py +0 -0
- {interhumanai-1.2.3 → interhumanai-1.3.2}/src/interhumanai/_sdk_metadata.py +0 -0
- {interhumanai-1.2.3 → interhumanai-1.3.2}/src/interhumanai/_session_socket.py +0 -0
- {interhumanai-1.2.3 → interhumanai-1.3.2}/src/interhumanai/auth.py +0 -0
- {interhumanai-1.2.3 → interhumanai-1.3.2}/src/interhumanai/config.py +0 -0
- {interhumanai-1.2.3 → interhumanai-1.3.2}/src/interhumanai/errors.py +0 -0
- {interhumanai-1.2.3 → interhumanai-1.3.2}/src/interhumanai/py.typed +0 -0
- {interhumanai-1.2.3 → interhumanai-1.3.2}/src/interhumanai/realtime.py +0 -0
|
@@ -5,6 +5,37 @@ 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
|
+
|
|
28
|
+
## 1.3.0
|
|
29
|
+
|
|
30
|
+
- Added the whole-file signal list to upload jobs. ``submit(include=...)``
|
|
31
|
+
now takes ``UploadJobIncludeFlag``: the two ``IncludeFlag`` Conversation
|
|
32
|
+
Quality Index values plus ``SIGNALS`` (``IncludeFlag`` members still work).
|
|
33
|
+
With ``SIGNALS``, a completed job's ``UploadJobResult.signals`` carries every
|
|
34
|
+
window's signals as one chronological list, same-type signals in adjacent
|
|
35
|
+
windows merged into one span that keeps the highest probability and its
|
|
36
|
+
rationale, with no ``modality``. ``None`` without the flag; an empty list
|
|
37
|
+
when requested and nothing was detected. The ``windows`` are unchanged.
|
|
38
|
+
|
|
8
39
|
## 1.2.3
|
|
9
40
|
|
|
10
41
|
- Revised the reference documentation for authentication. Documentation only;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: interhumanai
|
|
3
|
-
Version: 1.2
|
|
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,40 +128,19 @@ 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
|
-
from interhumanai import
|
|
137
|
+
from interhumanai import UploadJobIncludeFlag, UploadModel
|
|
148
138
|
|
|
149
139
|
job = await client.upload.submit(
|
|
150
140
|
"meeting.mp4",
|
|
151
141
|
model=UploadModel.INTER_2,
|
|
152
|
-
|
|
142
|
+
# optional whole-file views: quality scores and the merged signal list
|
|
143
|
+
include=[UploadJobIncludeFlag.CONVERSATION_QUALITY_OVERALL, UploadJobIncludeFlag.SIGNALS],
|
|
153
144
|
)
|
|
154
145
|
print(job.job_id, job.status.value) # queued
|
|
155
146
|
|
|
@@ -158,6 +149,8 @@ if job.status.value == "completed":
|
|
|
158
149
|
for window in job.result.windows:
|
|
159
150
|
names = ", ".join(signal.type.value for signal in window.signals)
|
|
160
151
|
print(f"{window.start_seconds:5.1f}s {window.engagement_status.value:11} {names}")
|
|
152
|
+
for signal in job.result.signals:
|
|
153
|
+
print(signal.type.value, signal.start, signal.end)
|
|
161
154
|
print(job.result.conversation_quality.overall.quality_index)
|
|
162
155
|
else:
|
|
163
156
|
print(job.error.error_id, job.error.message)
|
|
@@ -166,11 +159,15 @@ else:
|
|
|
166
159
|
- `submit(file, model=..., wait_seconds=0, include=None)` — pass `wait_seconds`
|
|
167
160
|
to let the API hold the request open for up to that long and return the
|
|
168
161
|
finished job inline when it completes in time; otherwise it returns the
|
|
169
|
-
queued envelope. Pass `include` with
|
|
170
|
-
|
|
171
|
-
`
|
|
172
|
-
|
|
173
|
-
|
|
162
|
+
queued envelope. Pass `include` with `UploadJobIncludeFlag` values to have
|
|
163
|
+
the result carry whole-file views. The two Conversation Quality Index values
|
|
164
|
+
(the same ones `analyze` takes as `IncludeFlag`, whose members also work
|
|
165
|
+
here) add `conversation_quality`, an `UploadJobConversationQuality` scored
|
|
166
|
+
over the whole file with `overall` and/or `timeline` set for the flags you
|
|
167
|
+
named. `SIGNALS` adds `signals`: every window's signals as one chronological
|
|
168
|
+
list, same-type signals in adjacent windows merged into one span with the
|
|
169
|
+
highest probability and its rationale, and no `modality` (an empty list when
|
|
170
|
+
nothing was detected). Without a flag the result has no such section.
|
|
174
171
|
- `get_job(job_id)` — one status read. Raises `InterhumanAPIError` with
|
|
175
172
|
`error_id` `ih4021` once the job has expired (`job.expires_at`).
|
|
176
173
|
- `wait_for_job(job_id, timeout=None, poll_interval=2.0)` — returns the
|
|
@@ -183,17 +180,50 @@ else:
|
|
|
183
180
|
`UploadJobWindow` per fixed-length window, each with its span, an
|
|
184
181
|
`engagement_status` and the `signals` read over it. Each signal is an
|
|
185
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.
|
|
186
215
|
|
|
187
216
|
## Stream API
|
|
188
217
|
|
|
189
|
-
One `StreamClient` handles one live session
|
|
190
|
-
|
|
191
|
-
|
|
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`:
|
|
192
222
|
|
|
193
223
|
```python
|
|
194
224
|
from interhumanai import IncludeFlag, SignalDetectedEvent
|
|
195
225
|
|
|
196
|
-
async with client.stream() as session:
|
|
226
|
+
async with client.stream(api_version="v2") as session:
|
|
197
227
|
await session.wait_for_session_ready()
|
|
198
228
|
await session.update_config(include=[IncludeFlag.CONVERSATION_QUALITY_OVERALL])
|
|
199
229
|
await session.send_video(first_chunk) # first chunk carries the container header
|
|
@@ -209,31 +239,26 @@ close code and reason. Event types this SDK version does not know arrive as
|
|
|
209
239
|
|
|
210
240
|
### Choosing the model: v1 and v2
|
|
211
241
|
|
|
212
|
-
`client.stream()` opens `WS /v1/stream/analyze`,
|
|
213
|
-
model
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
more thing differs: how the client names itself: errors raised for a v2 session say "Inter-2
|
|
227
|
-
stream" where a v1 session says "Stream", so a message names the endpoint the
|
|
228
|
-
session actually opened. If a deployment has no Inter-2 backend configured, a
|
|
229
|
-
v2 session is refused right after the handshake with an `ErrorEvent`
|
|
230
|
-
(`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.
|
|
231
256
|
|
|
232
257
|
### Choosing the Inter-2 model
|
|
233
258
|
|
|
234
259
|
On `api_version="v2"` the `model` argument names the model the session opens
|
|
235
|
-
on. It defaults to `StreamModel.INTER_2`, which reads the video
|
|
236
|
-
`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
|
|
237
262
|
that your credential may select under `supported_session_config_options.model`,
|
|
238
263
|
and `session.updated` echoes the one in force.
|
|
239
264
|
|
|
@@ -244,6 +269,23 @@ async with client.stream(api_version="v2", model=StreamModel.INTER_2) as session
|
|
|
244
269
|
await session.wait_for_session_ready()
|
|
245
270
|
```
|
|
246
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
|
+
|
|
247
289
|
## Errors
|
|
248
290
|
|
|
249
291
|
Three surfaces, mirroring the API:
|
|
@@ -287,17 +329,18 @@ want to inspect exactly what is sent.
|
|
|
287
329
|
## Environments
|
|
288
330
|
|
|
289
331
|
```python
|
|
290
|
-
InterhumanClient(
|
|
291
|
-
InterhumanClient(
|
|
292
|
-
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
|
|
293
335
|
```
|
|
294
336
|
|
|
295
337
|
WebSocket URLs are derived automatically (`https://` → `wss://`).
|
|
296
338
|
|
|
297
339
|
## Examples
|
|
298
340
|
|
|
299
|
-
Runnable scripts for every flow live in [`examples/`](examples/):
|
|
300
|
-
`
|
|
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).
|
|
301
344
|
|
|
302
345
|
## Development
|
|
303
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,40 +102,19 @@ 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
|
-
from interhumanai import
|
|
111
|
+
from interhumanai import UploadJobIncludeFlag, UploadModel
|
|
122
112
|
|
|
123
113
|
job = await client.upload.submit(
|
|
124
114
|
"meeting.mp4",
|
|
125
115
|
model=UploadModel.INTER_2,
|
|
126
|
-
|
|
116
|
+
# optional whole-file views: quality scores and the merged signal list
|
|
117
|
+
include=[UploadJobIncludeFlag.CONVERSATION_QUALITY_OVERALL, UploadJobIncludeFlag.SIGNALS],
|
|
127
118
|
)
|
|
128
119
|
print(job.job_id, job.status.value) # queued
|
|
129
120
|
|
|
@@ -132,6 +123,8 @@ if job.status.value == "completed":
|
|
|
132
123
|
for window in job.result.windows:
|
|
133
124
|
names = ", ".join(signal.type.value for signal in window.signals)
|
|
134
125
|
print(f"{window.start_seconds:5.1f}s {window.engagement_status.value:11} {names}")
|
|
126
|
+
for signal in job.result.signals:
|
|
127
|
+
print(signal.type.value, signal.start, signal.end)
|
|
135
128
|
print(job.result.conversation_quality.overall.quality_index)
|
|
136
129
|
else:
|
|
137
130
|
print(job.error.error_id, job.error.message)
|
|
@@ -140,11 +133,15 @@ else:
|
|
|
140
133
|
- `submit(file, model=..., wait_seconds=0, include=None)` — pass `wait_seconds`
|
|
141
134
|
to let the API hold the request open for up to that long and return the
|
|
142
135
|
finished job inline when it completes in time; otherwise it returns the
|
|
143
|
-
queued envelope. Pass `include` with
|
|
144
|
-
|
|
145
|
-
`
|
|
146
|
-
|
|
147
|
-
|
|
136
|
+
queued envelope. Pass `include` with `UploadJobIncludeFlag` values to have
|
|
137
|
+
the result carry whole-file views. The two Conversation Quality Index values
|
|
138
|
+
(the same ones `analyze` takes as `IncludeFlag`, whose members also work
|
|
139
|
+
here) add `conversation_quality`, an `UploadJobConversationQuality` scored
|
|
140
|
+
over the whole file with `overall` and/or `timeline` set for the flags you
|
|
141
|
+
named. `SIGNALS` adds `signals`: every window's signals as one chronological
|
|
142
|
+
list, same-type signals in adjacent windows merged into one span with the
|
|
143
|
+
highest probability and its rationale, and no `modality` (an empty list when
|
|
144
|
+
nothing was detected). Without a flag the result has no such section.
|
|
148
145
|
- `get_job(job_id)` — one status read. Raises `InterhumanAPIError` with
|
|
149
146
|
`error_id` `ih4021` once the job has expired (`job.expires_at`).
|
|
150
147
|
- `wait_for_job(job_id, timeout=None, poll_interval=2.0)` — returns the
|
|
@@ -157,17 +154,50 @@ else:
|
|
|
157
154
|
`UploadJobWindow` per fixed-length window, each with its span, an
|
|
158
155
|
`engagement_status` and the `signals` read over it. Each signal is an
|
|
159
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.
|
|
160
189
|
|
|
161
190
|
## Stream API
|
|
162
191
|
|
|
163
|
-
One `StreamClient` handles one live session
|
|
164
|
-
|
|
165
|
-
|
|
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`:
|
|
166
196
|
|
|
167
197
|
```python
|
|
168
198
|
from interhumanai import IncludeFlag, SignalDetectedEvent
|
|
169
199
|
|
|
170
|
-
async with client.stream() as session:
|
|
200
|
+
async with client.stream(api_version="v2") as session:
|
|
171
201
|
await session.wait_for_session_ready()
|
|
172
202
|
await session.update_config(include=[IncludeFlag.CONVERSATION_QUALITY_OVERALL])
|
|
173
203
|
await session.send_video(first_chunk) # first chunk carries the container header
|
|
@@ -183,31 +213,26 @@ close code and reason. Event types this SDK version does not know arrive as
|
|
|
183
213
|
|
|
184
214
|
### Choosing the model: v1 and v2
|
|
185
215
|
|
|
186
|
-
`client.stream()` opens `WS /v1/stream/analyze`,
|
|
187
|
-
model
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
more thing differs: how the client names itself: errors raised for a v2 session say "Inter-2
|
|
201
|
-
stream" where a v1 session says "Stream", so a message names the endpoint the
|
|
202
|
-
session actually opened. If a deployment has no Inter-2 backend configured, a
|
|
203
|
-
v2 session is refused right after the handshake with an `ErrorEvent`
|
|
204
|
-
(`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.
|
|
205
230
|
|
|
206
231
|
### Choosing the Inter-2 model
|
|
207
232
|
|
|
208
233
|
On `api_version="v2"` the `model` argument names the model the session opens
|
|
209
|
-
on. It defaults to `StreamModel.INTER_2`, which reads the video
|
|
210
|
-
`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
|
|
211
236
|
that your credential may select under `supported_session_config_options.model`,
|
|
212
237
|
and `session.updated` echoes the one in force.
|
|
213
238
|
|
|
@@ -218,6 +243,23 @@ async with client.stream(api_version="v2", model=StreamModel.INTER_2) as session
|
|
|
218
243
|
await session.wait_for_session_ready()
|
|
219
244
|
```
|
|
220
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
|
+
|
|
221
263
|
## Errors
|
|
222
264
|
|
|
223
265
|
Three surfaces, mirroring the API:
|
|
@@ -261,17 +303,18 @@ want to inspect exactly what is sent.
|
|
|
261
303
|
## Environments
|
|
262
304
|
|
|
263
305
|
```python
|
|
264
|
-
InterhumanClient(
|
|
265
|
-
InterhumanClient(
|
|
266
|
-
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
|
|
267
309
|
```
|
|
268
310
|
|
|
269
311
|
WebSocket URLs are derived automatically (`https://` → `wss://`).
|
|
270
312
|
|
|
271
313
|
## Examples
|
|
272
314
|
|
|
273
|
-
Runnable scripts for every flow live in [`examples/`](examples/):
|
|
274
|
-
`
|
|
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).
|
|
275
318
|
|
|
276
319
|
## Development
|
|
277
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
|
|
@@ -113,6 +125,7 @@ from .types import (
|
|
|
113
125
|
UploadJob,
|
|
114
126
|
UploadJobConversationQuality,
|
|
115
127
|
UploadJobError,
|
|
128
|
+
UploadJobIncludeFlag,
|
|
116
129
|
UploadJobResult,
|
|
117
130
|
UploadJobSignal,
|
|
118
131
|
UploadJobStatus,
|
|
@@ -214,6 +227,7 @@ __all__ = [
|
|
|
214
227
|
"UploadJob",
|
|
215
228
|
"UploadJobConversationQuality",
|
|
216
229
|
"UploadJobError",
|
|
230
|
+
"UploadJobIncludeFlag",
|
|
217
231
|
"UploadJobResult",
|
|
218
232
|
"UploadJobSignal",
|
|
219
233
|
"UploadJobStatus",
|
|
@@ -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"
|
|
@@ -226,6 +240,20 @@ class UploadModel(str, enum.Enum):
|
|
|
226
240
|
INTER_2_DEEP = "inter-2-deep"
|
|
227
241
|
|
|
228
242
|
|
|
243
|
+
class UploadJobIncludeFlag(str, enum.Enum):
|
|
244
|
+
"""Whole-file sections a ``POST /v2/upload/analyze`` job's result can carry.
|
|
245
|
+
|
|
246
|
+
The two :class:`IncludeFlag` Conversation Quality Index sections, plus
|
|
247
|
+
``SIGNALS`` for the whole-file signal list
|
|
248
|
+
(:attr:`UploadJobResult.signals`). ``SIGNALS`` is a v2 upload option only;
|
|
249
|
+
:meth:`~interhumanai.UploadClient.analyze` and the stream do not take it.
|
|
250
|
+
"""
|
|
251
|
+
|
|
252
|
+
CONVERSATION_QUALITY_OVERALL = "conversation_quality_overall"
|
|
253
|
+
CONVERSATION_QUALITY_TIMELINE = "conversation_quality_timeline"
|
|
254
|
+
SIGNALS = "signals"
|
|
255
|
+
|
|
256
|
+
|
|
229
257
|
class UploadJobStatus(str, enum.Enum):
|
|
230
258
|
"""Lifecycle of a ``POST /v2/upload/analyze`` job.
|
|
231
259
|
|
|
@@ -285,13 +313,22 @@ class UploadJobConversationQuality(BaseModel):
|
|
|
285
313
|
class UploadJobResult(BaseModel):
|
|
286
314
|
"""The result of a completed upload job: one entry per analyzed window.
|
|
287
315
|
|
|
316
|
+
``signals`` is every window's signals as one list over the whole file, in
|
|
317
|
+
chronological order, merged by the same rules as
|
|
318
|
+
:meth:`~interhumanai.UploadClient.analyze` and with no ``modality``:
|
|
319
|
+
signals of the same type in adjacent windows become one span, which keeps
|
|
320
|
+
the highest probability and that signal's rationale. It is set only when the submit
|
|
321
|
+
passed :attr:`UploadJobIncludeFlag.SIGNALS`, and is then an empty list when
|
|
322
|
+
nothing was detected; ``windows`` are unchanged either way.
|
|
323
|
+
|
|
288
324
|
``conversation_quality`` is present only when the submit passed at least one
|
|
289
|
-
|
|
325
|
+
Conversation Quality Index flag, and carries the sections those flags named.
|
|
290
326
|
"""
|
|
291
327
|
|
|
292
328
|
duration_seconds: float
|
|
293
329
|
window_seconds: float
|
|
294
330
|
windows: list[UploadJobWindow] = Field(default_factory=list)
|
|
331
|
+
signals: list[UploadJobSignal] | None = None
|
|
295
332
|
conversation_quality: UploadJobConversationQuality | None = None
|
|
296
333
|
|
|
297
334
|
|
|
@@ -13,7 +13,14 @@ from ._http import request_json
|
|
|
13
13
|
from .auth import TokenProvider
|
|
14
14
|
from .config import Environment, resolve_http_base_url
|
|
15
15
|
from .errors import UploadJobTimeoutError
|
|
16
|
-
from .types import
|
|
16
|
+
from .types import (
|
|
17
|
+
AnalysisResult,
|
|
18
|
+
GoalDimension,
|
|
19
|
+
IncludeFlag,
|
|
20
|
+
UploadJob,
|
|
21
|
+
UploadJobIncludeFlag,
|
|
22
|
+
UploadModel,
|
|
23
|
+
)
|
|
17
24
|
|
|
18
25
|
DEFAULT_JOB_POLL_INTERVAL_SECONDS = 2.0
|
|
19
26
|
"""How often :meth:`UploadClient.wait_for_job` reads the job status by default."""
|
|
@@ -119,7 +126,7 @@ class UploadClient:
|
|
|
119
126
|
*,
|
|
120
127
|
model: UploadModel | str,
|
|
121
128
|
wait_seconds: int = 0,
|
|
122
|
-
include: Sequence[IncludeFlag | str] | None = None,
|
|
129
|
+
include: Sequence[UploadJobIncludeFlag | IncludeFlag | str] | None = None,
|
|
123
130
|
filename: str | None = None,
|
|
124
131
|
content_type: str | None = None,
|
|
125
132
|
) -> UploadJob:
|
|
@@ -132,10 +139,11 @@ class UploadClient:
|
|
|
132
139
|
it comes back ``queued`` or ``running`` and :meth:`wait_for_job` or
|
|
133
140
|
:meth:`get_job` reads it later.
|
|
134
141
|
|
|
135
|
-
Pass ``include`` to have the completed job's result carry
|
|
136
|
-
Conversation Quality Index
|
|
137
|
-
|
|
138
|
-
|
|
142
|
+
Pass ``include`` to have the completed job's result carry whole-file
|
|
143
|
+
views: the Conversation Quality Index in the sections the CQI flags
|
|
144
|
+
name (:attr:`UploadJobResult.conversation_quality`), and the merged
|
|
145
|
+
signal list for ``SIGNALS`` (:attr:`UploadJobResult.signals`). Without
|
|
146
|
+
a flag the result carries no such section.
|
|
139
147
|
|
|
140
148
|
For ``UploadModel.INTER_2`` the file is an mp4, mov, avi, mkv, webm or
|
|
141
149
|
mpeg-ts video carrying both a video and an audio track (``ih5001``
|
|
@@ -151,10 +159,13 @@ class UploadClient:
|
|
|
151
159
|
wait_seconds: How long the API may hold the request waiting for
|
|
152
160
|
the job to finish. ``0`` (the default) answers at once. The
|
|
153
161
|
deployment bounds it; a larger value raises ``ih4005``.
|
|
154
|
-
include:
|
|
155
|
-
|
|
156
|
-
``
|
|
157
|
-
|
|
162
|
+
include: Whole-file sections the result should carry, as
|
|
163
|
+
:class:`UploadJobIncludeFlag` values:
|
|
164
|
+
``CONVERSATION_QUALITY_OVERALL`` and/or
|
|
165
|
+
``CONVERSATION_QUALITY_TIMELINE`` (the same values
|
|
166
|
+
:meth:`analyze` takes, so :class:`IncludeFlag` members work
|
|
167
|
+
too) for the quality block, and ``SIGNALS`` for the signal
|
|
168
|
+
list. Omit a flag for no such section.
|
|
158
169
|
filename: Filename reported to the API. Defaults to the path or
|
|
159
170
|
file object's name, else ``video``.
|
|
160
171
|
content_type: MIME type of the file (e.g. ``video/mp4``).
|
|
@@ -172,7 +183,8 @@ class UploadClient:
|
|
|
172
183
|
data["wait_seconds"] = str(wait_seconds)
|
|
173
184
|
if include:
|
|
174
185
|
data["include[]"] = [
|
|
175
|
-
flag.value if isinstance(flag, IncludeFlag) else flag
|
|
186
|
+
flag.value if isinstance(flag, (IncludeFlag, UploadJobIncludeFlag)) else flag
|
|
187
|
+
for flag in include
|
|
176
188
|
]
|
|
177
189
|
headers = {"Authorization": f"Bearer {token}"}
|
|
178
190
|
url = self._base_url + "/v2/upload/analyze"
|
|
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
|