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.
Files changed (20) hide show
  1. {interhumanai-1.2.3 → interhumanai-1.3.2}/CHANGELOG.md +31 -0
  2. {interhumanai-1.2.3 → interhumanai-1.3.2}/PKG-INFO +125 -82
  3. {interhumanai-1.2.3 → interhumanai-1.3.2}/README.md +124 -81
  4. {interhumanai-1.2.3 → interhumanai-1.3.2}/src/interhumanai/__init__.py +19 -5
  5. {interhumanai-1.2.3 → interhumanai-1.3.2}/src/interhumanai/_version.py +1 -1
  6. {interhumanai-1.2.3 → interhumanai-1.3.2}/src/interhumanai/client.py +5 -5
  7. {interhumanai-1.2.3 → interhumanai-1.3.2}/src/interhumanai/stream.py +16 -16
  8. {interhumanai-1.2.3 → interhumanai-1.3.2}/src/interhumanai/types.py +46 -9
  9. {interhumanai-1.2.3 → interhumanai-1.3.2}/src/interhumanai/upload.py +23 -11
  10. {interhumanai-1.2.3 → interhumanai-1.3.2}/.gitignore +0 -0
  11. {interhumanai-1.2.3 → interhumanai-1.3.2}/LICENSE +0 -0
  12. {interhumanai-1.2.3 → interhumanai-1.3.2}/pyproject.toml +0 -0
  13. {interhumanai-1.2.3 → interhumanai-1.3.2}/src/interhumanai/_http.py +0 -0
  14. {interhumanai-1.2.3 → interhumanai-1.3.2}/src/interhumanai/_sdk_metadata.py +0 -0
  15. {interhumanai-1.2.3 → interhumanai-1.3.2}/src/interhumanai/_session_socket.py +0 -0
  16. {interhumanai-1.2.3 → interhumanai-1.3.2}/src/interhumanai/auth.py +0 -0
  17. {interhumanai-1.2.3 → interhumanai-1.3.2}/src/interhumanai/config.py +0 -0
  18. {interhumanai-1.2.3 → interhumanai-1.3.2}/src/interhumanai/errors.py +0 -0
  19. {interhumanai-1.2.3 → interhumanai-1.3.2}/src/interhumanai/py.typed +0 -0
  20. {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
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,40 +128,19 @@ 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
- from interhumanai import IncludeFlag, UploadModel
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
- include=[IncludeFlag.CONVERSATION_QUALITY_OVERALL], # optional quality scores
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 the same `IncludeFlag` values `analyze`
170
- takes to have the result carry `conversation_quality`, an
171
- `UploadJobConversationQuality` scored over the whole file with `overall`
172
- and/or `timeline` set for the flags you named; without it the result has no
173
- quality block.
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 against `WS /v1/stream/analyze`.
190
- Send binary WebM or fragmented-MP4 chunks and consume typed events with
191
- `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`:
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`, analyzed by the Inter-1
213
- model. Pass `api_version="v2"` to open `WS /v2/stream/analyze` and have the
214
- same session analyzed by the Inter-2 model:
215
-
216
- ```python
217
- async with client.stream(api_version="v2") as session:
218
- ...
219
- ```
220
-
221
- Everything else is identical — the `session.ready` limits, session config, the
222
- video you send, the events you receive and their order, the same `Scope.STREAM`
223
- scope for the default model, and the graceful close — so the code above works
224
- unchanged, except that
225
- v2 signal events carry no `modality` (`event.data.modality` is `None`). One
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 and which
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(key_id=..., key_secret=...) # production (default): api.interhuman.ai
291
- InterhumanClient(key_id=..., key_secret=..., environment="staging") # staging-api.interhuman.ai
292
- 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
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/): `auth.py`,
300
- `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).
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
- 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,40 +102,19 @@ 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
- from interhumanai import IncludeFlag, UploadModel
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
- include=[IncludeFlag.CONVERSATION_QUALITY_OVERALL], # optional quality scores
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 the same `IncludeFlag` values `analyze`
144
- takes to have the result carry `conversation_quality`, an
145
- `UploadJobConversationQuality` scored over the whole file with `overall`
146
- and/or `timeline` set for the flags you named; without it the result has no
147
- quality block.
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 against `WS /v1/stream/analyze`.
164
- Send binary WebM or fragmented-MP4 chunks and consume typed events with
165
- `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`:
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`, analyzed by the Inter-1
187
- model. Pass `api_version="v2"` to open `WS /v2/stream/analyze` and have the
188
- same session analyzed by the Inter-2 model:
189
-
190
- ```python
191
- async with client.stream(api_version="v2") as session:
192
- ...
193
- ```
194
-
195
- Everything else is identical — the `session.ready` limits, session config, the
196
- video you send, the events you receive and their order, the same `Scope.STREAM`
197
- scope for the default model, and the graceful close — so the code above works
198
- unchanged, except that
199
- v2 signal events carry no `modality` (`event.data.modality` is `None`). One
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 and which
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(key_id=..., key_secret=...) # production (default): api.interhuman.ai
265
- InterhumanClient(key_id=..., key_secret=..., environment="staging") # staging-api.interhuman.ai
266
- 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
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/): `auth.py`,
274
- `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).
275
318
 
276
319
  ## Development
277
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
@@ -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",
@@ -1,3 +1,3 @@
1
1
  """Package version (single source of truth, read by hatchling at build time)."""
2
2
 
3
- __version__ = "1.2.3"
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"
@@ -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
- :class:`IncludeFlag`, and carries the sections those flags named.
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 AnalysisResult, GoalDimension, IncludeFlag, UploadJob, UploadModel
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 the
136
- Conversation Quality Index over the whole file, in the sections the
137
- flags name (:attr:`UploadJobResult.conversation_quality`). Without it
138
- the result carries no such block.
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: Conversation Quality Index sections the result should
155
- carry: ``IncludeFlag.CONVERSATION_QUALITY_OVERALL`` and/or
156
- ``IncludeFlag.CONVERSATION_QUALITY_TIMELINE``, the same flags
157
- :meth:`analyze` takes. Omit it for no quality block.
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 for flag in include
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