interhumanai 1.2.2__tar.gz → 1.3.0__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.
@@ -5,6 +5,22 @@ 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.0
9
+
10
+ - Added the whole-file signal list to upload jobs. ``submit(include=...)``
11
+ now takes ``UploadJobIncludeFlag``: the two ``IncludeFlag`` Conversation
12
+ Quality Index values plus ``SIGNALS`` (``IncludeFlag`` members still work).
13
+ With ``SIGNALS``, a completed job's ``UploadJobResult.signals`` carries every
14
+ window's signals as one chronological list, same-type signals in adjacent
15
+ windows merged into one span that keeps the highest probability and its
16
+ rationale, with no ``modality``. ``None`` without the flag; an empty list
17
+ when requested and nothing was detected. The ``windows`` are unchanged.
18
+
19
+ ## 1.2.3
20
+
21
+ - Revised the reference documentation for authentication. Documentation only;
22
+ no behavior change.
23
+
8
24
  ## 1.2.2
9
25
 
10
26
  - Corrected the reference docs for ``SessionConfigOptions.model``: it lists the models the
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: interhumanai
3
- Version: 1.2.2
3
+ Version: 1.3.0
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
@@ -144,12 +144,13 @@ job**: the API validates the file, answers with a job envelope, and analyzes
144
144
  the file afterwards. Poll the job until it is `completed` or `failed`:
145
145
 
146
146
  ```python
147
- from interhumanai import IncludeFlag, UploadModel
147
+ from interhumanai import UploadJobIncludeFlag, UploadModel
148
148
 
149
149
  job = await client.upload.submit(
150
150
  "meeting.mp4",
151
151
  model=UploadModel.INTER_2,
152
- include=[IncludeFlag.CONVERSATION_QUALITY_OVERALL], # optional quality scores
152
+ # optional whole-file views: quality scores and the merged signal list
153
+ include=[UploadJobIncludeFlag.CONVERSATION_QUALITY_OVERALL, UploadJobIncludeFlag.SIGNALS],
153
154
  )
154
155
  print(job.job_id, job.status.value) # queued
155
156
 
@@ -158,6 +159,8 @@ if job.status.value == "completed":
158
159
  for window in job.result.windows:
159
160
  names = ", ".join(signal.type.value for signal in window.signals)
160
161
  print(f"{window.start_seconds:5.1f}s {window.engagement_status.value:11} {names}")
162
+ for signal in job.result.signals:
163
+ print(signal.type.value, signal.start, signal.end)
161
164
  print(job.result.conversation_quality.overall.quality_index)
162
165
  else:
163
166
  print(job.error.error_id, job.error.message)
@@ -166,11 +169,15 @@ else:
166
169
  - `submit(file, model=..., wait_seconds=0, include=None)` — pass `wait_seconds`
167
170
  to let the API hold the request open for up to that long and return the
168
171
  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.
172
+ queued envelope. Pass `include` with `UploadJobIncludeFlag` values to have
173
+ the result carry whole-file views. The two Conversation Quality Index values
174
+ (the same ones `analyze` takes as `IncludeFlag`, whose members also work
175
+ here) add `conversation_quality`, an `UploadJobConversationQuality` scored
176
+ over the whole file with `overall` and/or `timeline` set for the flags you
177
+ named. `SIGNALS` adds `signals`: every window's signals as one chronological
178
+ list, same-type signals in adjacent windows merged into one span with the
179
+ highest probability and its rationale, and no `modality` (an empty list when
180
+ nothing was detected). Without a flag the result has no such section.
174
181
  - `get_job(job_id)` — one status read. Raises `InterhumanAPIError` with
175
182
  `error_id` `ih4021` once the job has expired (`job.expires_at`).
176
183
  - `wait_for_job(job_id, timeout=None, poll_interval=2.0)` — returns the
@@ -118,12 +118,13 @@ job**: the API validates the file, answers with a job envelope, and analyzes
118
118
  the file afterwards. Poll the job until it is `completed` or `failed`:
119
119
 
120
120
  ```python
121
- from interhumanai import IncludeFlag, UploadModel
121
+ from interhumanai import UploadJobIncludeFlag, UploadModel
122
122
 
123
123
  job = await client.upload.submit(
124
124
  "meeting.mp4",
125
125
  model=UploadModel.INTER_2,
126
- include=[IncludeFlag.CONVERSATION_QUALITY_OVERALL], # optional quality scores
126
+ # optional whole-file views: quality scores and the merged signal list
127
+ include=[UploadJobIncludeFlag.CONVERSATION_QUALITY_OVERALL, UploadJobIncludeFlag.SIGNALS],
127
128
  )
128
129
  print(job.job_id, job.status.value) # queued
129
130
 
@@ -132,6 +133,8 @@ if job.status.value == "completed":
132
133
  for window in job.result.windows:
133
134
  names = ", ".join(signal.type.value for signal in window.signals)
134
135
  print(f"{window.start_seconds:5.1f}s {window.engagement_status.value:11} {names}")
136
+ for signal in job.result.signals:
137
+ print(signal.type.value, signal.start, signal.end)
135
138
  print(job.result.conversation_quality.overall.quality_index)
136
139
  else:
137
140
  print(job.error.error_id, job.error.message)
@@ -140,11 +143,15 @@ else:
140
143
  - `submit(file, model=..., wait_seconds=0, include=None)` — pass `wait_seconds`
141
144
  to let the API hold the request open for up to that long and return the
142
145
  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.
146
+ queued envelope. Pass `include` with `UploadJobIncludeFlag` values to have
147
+ the result carry whole-file views. The two Conversation Quality Index values
148
+ (the same ones `analyze` takes as `IncludeFlag`, whose members also work
149
+ here) add `conversation_quality`, an `UploadJobConversationQuality` scored
150
+ over the whole file with `overall` and/or `timeline` set for the flags you
151
+ named. `SIGNALS` adds `signals`: every window's signals as one chronological
152
+ list, same-type signals in adjacent windows merged into one span with the
153
+ highest probability and its rationale, and no `modality` (an empty list when
154
+ nothing was detected). Without a flag the result has no such section.
148
155
  - `get_job(job_id)` — one status read. Raises `InterhumanAPIError` with
149
156
  `error_id` `ih4021` once the job has expired (`job.expires_at`).
150
157
  - `wait_for_job(job_id, timeout=None, poll_interval=2.0)` — returns the
@@ -113,6 +113,7 @@ from .types import (
113
113
  UploadJob,
114
114
  UploadJobConversationQuality,
115
115
  UploadJobError,
116
+ UploadJobIncludeFlag,
116
117
  UploadJobResult,
117
118
  UploadJobSignal,
118
119
  UploadJobStatus,
@@ -214,6 +215,7 @@ __all__ = [
214
215
  "UploadJob",
215
216
  "UploadJobConversationQuality",
216
217
  "UploadJobError",
218
+ "UploadJobIncludeFlag",
217
219
  "UploadJobResult",
218
220
  "UploadJobSignal",
219
221
  "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.2"
3
+ __version__ = "1.3.0"
@@ -20,7 +20,7 @@ DEFAULT_REFRESH_SKEW_SECONDS = 30.0
20
20
 
21
21
 
22
22
  class TokenResponse(BaseModel):
23
- """Response of ``POST /v1/auth``."""
23
+ """Response of a token request."""
24
24
 
25
25
  access_token: str
26
26
  token_type: str
@@ -48,7 +48,7 @@ def _scope_values(scopes: Sequence[Scope | str]) -> list[str]:
48
48
 
49
49
 
50
50
  class AuthClient:
51
- """Client for the token endpoints (``/v1/auth`` and ``/v1/client_tokens``).
51
+ """Client for the client-token endpoints (``/v1/client_tokens``).
52
52
 
53
53
  Args:
54
54
  environment: Named environment to call. Defaults to ``production``.
@@ -89,7 +89,7 @@ class AuthClient:
89
89
  key_secret: str,
90
90
  scopes: Sequence[Scope | str],
91
91
  ) -> TokenResponse:
92
- """Exchange API key credentials for a short-lived bearer access token.
92
+ """Authenticate with API key credentials for a short-lived session.
93
93
 
94
94
  Args:
95
95
  key_id: The API key id.
@@ -163,10 +163,10 @@ class AuthClient:
163
163
 
164
164
 
165
165
  class TokenProvider(Protocol):
166
- """Anything that can produce a bearer access token on demand."""
166
+ """Anything that can produce a bearer credential on demand."""
167
167
 
168
168
  async def get_token(self) -> str:
169
- """Return a currently valid bearer access token."""
169
+ """Return a currently valid bearer credential."""
170
170
  ...
171
171
 
172
172
 
@@ -177,7 +177,7 @@ class StaticTokenProvider:
177
177
  """Store the token to hand out.
178
178
 
179
179
  Args:
180
- token: The bearer access token to return from :meth:`get_token`.
180
+ token: The client token or API key to return from :meth:`get_token`.
181
181
  """
182
182
  self._token = token
183
183
 
@@ -224,7 +224,7 @@ class TokenManager:
224
224
  self._lock = asyncio.Lock()
225
225
 
226
226
  async def get_token(self) -> str:
227
- """Return a valid access token, minting or refreshing when needed."""
227
+ """Return a valid bearer credential, renewing it when needed."""
228
228
  async with self._lock:
229
229
  if self._token is not None and self._clock() < self._refresh_at:
230
230
  return self._token
@@ -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: Pre-issued bearer token (JWT or client token).
38
+ access_token: A client token, or your API key, 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``.
@@ -226,6 +226,20 @@ class UploadModel(str, enum.Enum):
226
226
  INTER_2_DEEP = "inter-2-deep"
227
227
 
228
228
 
229
+ class UploadJobIncludeFlag(str, enum.Enum):
230
+ """Whole-file sections a ``POST /v2/upload/analyze`` job's result can carry.
231
+
232
+ The two :class:`IncludeFlag` Conversation Quality Index sections, plus
233
+ ``SIGNALS`` for the whole-file signal list
234
+ (:attr:`UploadJobResult.signals`). ``SIGNALS`` is a v2 upload option only;
235
+ :meth:`~interhumanai.UploadClient.analyze` and the stream do not take it.
236
+ """
237
+
238
+ CONVERSATION_QUALITY_OVERALL = "conversation_quality_overall"
239
+ CONVERSATION_QUALITY_TIMELINE = "conversation_quality_timeline"
240
+ SIGNALS = "signals"
241
+
242
+
229
243
  class UploadJobStatus(str, enum.Enum):
230
244
  """Lifecycle of a ``POST /v2/upload/analyze`` job.
231
245
 
@@ -285,13 +299,22 @@ class UploadJobConversationQuality(BaseModel):
285
299
  class UploadJobResult(BaseModel):
286
300
  """The result of a completed upload job: one entry per analyzed window.
287
301
 
302
+ ``signals`` is every window's signals as one list over the whole file, in
303
+ chronological order, merged by the same rules as
304
+ :meth:`~interhumanai.UploadClient.analyze` and with no ``modality``:
305
+ signals of the same type in adjacent windows become one span, which keeps
306
+ the highest probability and that signal's rationale. It is set only when the submit
307
+ passed :attr:`UploadJobIncludeFlag.SIGNALS`, and is then an empty list when
308
+ nothing was detected; ``windows`` are unchanged either way.
309
+
288
310
  ``conversation_quality`` is present only when the submit passed at least one
289
- :class:`IncludeFlag`, and carries the sections those flags named.
311
+ Conversation Quality Index flag, and carries the sections those flags named.
290
312
  """
291
313
 
292
314
  duration_seconds: float
293
315
  window_seconds: float
294
316
  windows: list[UploadJobWindow] = Field(default_factory=list)
317
+ signals: list[UploadJobSignal] | None = None
295
318
  conversation_quality: UploadJobConversationQuality | None = None
296
319
 
297
320
 
@@ -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