creacortex 0.1.0__py3-none-any.whl
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.
- creacortex/__init__.py +39 -0
- creacortex/client.py +213 -0
- creacortex/errors.py +78 -0
- creacortex/models.py +98 -0
- creacortex/py.typed +0 -0
- creacortex-0.1.0.dist-info/METADATA +106 -0
- creacortex-0.1.0.dist-info/RECORD +9 -0
- creacortex-0.1.0.dist-info/WHEEL +4 -0
- creacortex-0.1.0.dist-info/licenses/LICENSE +21 -0
creacortex/__init__.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Creacortex — Python client for the video-creative attention-analysis API.
|
|
2
|
+
|
|
3
|
+
from creacortex import Client
|
|
4
|
+
|
|
5
|
+
with Client("ace_...") as cx:
|
|
6
|
+
run = cx.analyze("ad.mp4", with_audio=True) # upload + run + wait
|
|
7
|
+
cx.get_report(run.run_id, "diagnose") # PDF bytes
|
|
8
|
+
"""
|
|
9
|
+
from .client import DEFAULT_BASE_URL, AsyncClient, Client
|
|
10
|
+
from .errors import (
|
|
11
|
+
ApiError,
|
|
12
|
+
AuthError,
|
|
13
|
+
CreacortexError,
|
|
14
|
+
InsufficientCreditsError,
|
|
15
|
+
NotFoundError,
|
|
16
|
+
RateLimitError,
|
|
17
|
+
RunFailedError,
|
|
18
|
+
WaitTimeout,
|
|
19
|
+
)
|
|
20
|
+
from .models import Account, Run, RunListItem
|
|
21
|
+
|
|
22
|
+
__version__ = "0.1.0"
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"DEFAULT_BASE_URL",
|
|
26
|
+
"Client",
|
|
27
|
+
"AsyncClient",
|
|
28
|
+
"Run",
|
|
29
|
+
"RunListItem",
|
|
30
|
+
"Account",
|
|
31
|
+
"CreacortexError",
|
|
32
|
+
"ApiError",
|
|
33
|
+
"AuthError",
|
|
34
|
+
"InsufficientCreditsError",
|
|
35
|
+
"RateLimitError",
|
|
36
|
+
"NotFoundError",
|
|
37
|
+
"RunFailedError",
|
|
38
|
+
"WaitTimeout",
|
|
39
|
+
]
|
creacortex/client.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Sync and async clients for the Creacortex API."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import os
|
|
6
|
+
import time
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
from .errors import RunFailedError, WaitTimeout, raise_for_status
|
|
13
|
+
from .models import Account, Run, RunListItem
|
|
14
|
+
|
|
15
|
+
DEFAULT_BASE_URL = "https://api.creacortex.ai"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _build_files(file: Any, filename: str | None, content_type: str) -> dict[str, Any]:
|
|
19
|
+
if isinstance(file, (str, os.PathLike)):
|
|
20
|
+
p = Path(file)
|
|
21
|
+
return {"file": (filename or p.name, p.read_bytes(), content_type)}
|
|
22
|
+
if isinstance(file, (bytes, bytearray)):
|
|
23
|
+
return {"file": (filename or "video.mp4", bytes(file), content_type)}
|
|
24
|
+
name = filename or os.path.basename(getattr(file, "name", "") or "video.mp4")
|
|
25
|
+
return {"file": (name, file, content_type)}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class _Base:
|
|
29
|
+
def __init__(self, api_key: str, base_url: str) -> None:
|
|
30
|
+
if not api_key:
|
|
31
|
+
raise ValueError("api_key is required")
|
|
32
|
+
self._key = api_key
|
|
33
|
+
self._base = base_url.rstrip("/")
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def _headers(self) -> dict[str, str]:
|
|
37
|
+
return {"X-API-Key": self._key}
|
|
38
|
+
|
|
39
|
+
def _url(self, path: str) -> str:
|
|
40
|
+
return f"{self._base}{path}"
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def _run(resp: httpx.Response) -> Run:
|
|
44
|
+
raise_for_status(resp)
|
|
45
|
+
return Run.from_api(resp.json())
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def _runs(resp: httpx.Response) -> list[RunListItem]:
|
|
49
|
+
raise_for_status(resp)
|
|
50
|
+
return [RunListItem.from_api(x) for x in resp.json()]
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def _account(resp: httpx.Response) -> Account:
|
|
54
|
+
raise_for_status(resp)
|
|
55
|
+
return Account.from_api(resp.json())
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def _bytes(resp: httpx.Response) -> bytes:
|
|
59
|
+
raise_for_status(resp)
|
|
60
|
+
return resp.content
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def _json(resp: httpx.Response) -> Any:
|
|
64
|
+
raise_for_status(resp)
|
|
65
|
+
return resp.json()
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def _video_id(resp: httpx.Response) -> str:
|
|
69
|
+
raise_for_status(resp)
|
|
70
|
+
return str(resp.json()["video_id"])
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class Client(_Base):
|
|
74
|
+
"""Blocking client. Use as a context manager or call `close()` when done."""
|
|
75
|
+
|
|
76
|
+
def __init__(self, api_key: str, *, base_url: str = DEFAULT_BASE_URL,
|
|
77
|
+
timeout: float = 60.0, transport: httpx.BaseTransport | None = None) -> None:
|
|
78
|
+
super().__init__(api_key, base_url)
|
|
79
|
+
self._http = httpx.Client(timeout=timeout, transport=transport, headers=self._headers)
|
|
80
|
+
|
|
81
|
+
def __enter__(self) -> Client:
|
|
82
|
+
return self
|
|
83
|
+
|
|
84
|
+
def __exit__(self, *_exc: object) -> None:
|
|
85
|
+
self.close()
|
|
86
|
+
|
|
87
|
+
def close(self) -> None:
|
|
88
|
+
self._http.close()
|
|
89
|
+
|
|
90
|
+
def upload_video(self, file: Any, *, filename: str | None = None,
|
|
91
|
+
content_type: str = "video/mp4") -> str:
|
|
92
|
+
files = _build_files(file, filename, content_type)
|
|
93
|
+
return self._video_id(self._http.post(self._url("/v1/videos"), files=files))
|
|
94
|
+
|
|
95
|
+
def create_run(self, video_id: str, *, creative_id: str | None = None,
|
|
96
|
+
with_audio: bool = False, audience: str | None = None) -> Run:
|
|
97
|
+
body = {"video_id": video_id, "creative_id": creative_id,
|
|
98
|
+
"with_audio": with_audio, "audience": audience}
|
|
99
|
+
return self._run(self._http.post(self._url("/v1/runs"), json=body))
|
|
100
|
+
|
|
101
|
+
def get_run(self, run_id: str) -> Run:
|
|
102
|
+
return self._run(self._http.get(self._url(f"/v1/runs/{run_id}")))
|
|
103
|
+
|
|
104
|
+
def list_runs(self) -> list[RunListItem]:
|
|
105
|
+
return self._runs(self._http.get(self._url("/v1/runs")))
|
|
106
|
+
|
|
107
|
+
def get_account(self) -> Account:
|
|
108
|
+
return self._account(self._http.get(self._url("/v1/account")))
|
|
109
|
+
|
|
110
|
+
def get_csv(self, run_id: str) -> bytes:
|
|
111
|
+
"""Per-second analysis CSV (raw bytes)."""
|
|
112
|
+
return self._bytes(self._http.get(self._url(f"/v1/runs/{run_id}/data.csv")))
|
|
113
|
+
|
|
114
|
+
def get_export(self, run_id: str) -> Any:
|
|
115
|
+
"""Gen-AI payload (parsed JSON)."""
|
|
116
|
+
return self._json(self._http.get(self._url(f"/v1/runs/{run_id}/export.json")))
|
|
117
|
+
|
|
118
|
+
def get_report(self, run_id: str, kind: str = "diagnose") -> bytes:
|
|
119
|
+
"""PDF report bytes. `kind` is 'diagnose' or 'analyze'."""
|
|
120
|
+
return self._bytes(self._http.get(
|
|
121
|
+
self._url(f"/v1/runs/{run_id}/report.pdf"), params={"kind": kind}))
|
|
122
|
+
|
|
123
|
+
def wait(self, run_id: str, *, interval: float = 5.0, timeout: float = 1800.0) -> Run:
|
|
124
|
+
"""Poll until the run reaches a terminal state. Raises on failure/timeout."""
|
|
125
|
+
deadline = time.monotonic() + timeout
|
|
126
|
+
while True:
|
|
127
|
+
run = self.get_run(run_id)
|
|
128
|
+
if run.failed:
|
|
129
|
+
raise RunFailedError(run.run_id, run.error)
|
|
130
|
+
if run.done:
|
|
131
|
+
return run
|
|
132
|
+
if time.monotonic() >= deadline:
|
|
133
|
+
raise WaitTimeout(f"run {run_id} not done after {timeout:.0f}s")
|
|
134
|
+
time.sleep(interval)
|
|
135
|
+
|
|
136
|
+
def analyze(self, file: Any, *, creative_id: str | None = None, with_audio: bool = False,
|
|
137
|
+
audience: str | None = None, filename: str | None = None, wait: bool = True,
|
|
138
|
+
interval: float = 5.0, timeout: float = 1800.0) -> Run:
|
|
139
|
+
"""Upload, start a run, and (by default) wait for it to finish. Returns the Run."""
|
|
140
|
+
video_id = self.upload_video(file, filename=filename)
|
|
141
|
+
run = self.create_run(video_id, creative_id=creative_id,
|
|
142
|
+
with_audio=with_audio, audience=audience)
|
|
143
|
+
return self.wait(run.run_id, interval=interval, timeout=timeout) if wait else run
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class AsyncClient(_Base):
|
|
147
|
+
"""Async client. Use `async with` or `await aclose()`."""
|
|
148
|
+
|
|
149
|
+
def __init__(self, api_key: str, *, base_url: str = DEFAULT_BASE_URL,
|
|
150
|
+
timeout: float = 60.0, transport: httpx.AsyncBaseTransport | None = None) -> None:
|
|
151
|
+
super().__init__(api_key, base_url)
|
|
152
|
+
self._http = httpx.AsyncClient(timeout=timeout, transport=transport, headers=self._headers)
|
|
153
|
+
|
|
154
|
+
async def __aenter__(self) -> AsyncClient:
|
|
155
|
+
return self
|
|
156
|
+
|
|
157
|
+
async def __aexit__(self, *_exc: object) -> None:
|
|
158
|
+
await self.aclose()
|
|
159
|
+
|
|
160
|
+
async def aclose(self) -> None:
|
|
161
|
+
await self._http.aclose()
|
|
162
|
+
|
|
163
|
+
async def upload_video(self, file: Any, *, filename: str | None = None,
|
|
164
|
+
content_type: str = "video/mp4") -> str:
|
|
165
|
+
files = _build_files(file, filename, content_type)
|
|
166
|
+
return self._video_id(await self._http.post(self._url("/v1/videos"), files=files))
|
|
167
|
+
|
|
168
|
+
async def create_run(self, video_id: str, *, creative_id: str | None = None,
|
|
169
|
+
with_audio: bool = False, audience: str | None = None) -> Run:
|
|
170
|
+
body = {"video_id": video_id, "creative_id": creative_id,
|
|
171
|
+
"with_audio": with_audio, "audience": audience}
|
|
172
|
+
return self._run(await self._http.post(self._url("/v1/runs"), json=body))
|
|
173
|
+
|
|
174
|
+
async def get_run(self, run_id: str) -> Run:
|
|
175
|
+
return self._run(await self._http.get(self._url(f"/v1/runs/{run_id}")))
|
|
176
|
+
|
|
177
|
+
async def list_runs(self) -> list[RunListItem]:
|
|
178
|
+
return self._runs(await self._http.get(self._url("/v1/runs")))
|
|
179
|
+
|
|
180
|
+
async def get_account(self) -> Account:
|
|
181
|
+
return self._account(await self._http.get(self._url("/v1/account")))
|
|
182
|
+
|
|
183
|
+
async def get_csv(self, run_id: str) -> bytes:
|
|
184
|
+
return self._bytes(await self._http.get(self._url(f"/v1/runs/{run_id}/data.csv")))
|
|
185
|
+
|
|
186
|
+
async def get_export(self, run_id: str) -> Any:
|
|
187
|
+
return self._json(await self._http.get(self._url(f"/v1/runs/{run_id}/export.json")))
|
|
188
|
+
|
|
189
|
+
async def get_report(self, run_id: str, kind: str = "diagnose") -> bytes:
|
|
190
|
+
return self._bytes(await self._http.get(
|
|
191
|
+
self._url(f"/v1/runs/{run_id}/report.pdf"), params={"kind": kind}))
|
|
192
|
+
|
|
193
|
+
async def wait(self, run_id: str, *, interval: float = 5.0, timeout: float = 1800.0) -> Run:
|
|
194
|
+
deadline = time.monotonic() + timeout
|
|
195
|
+
while True:
|
|
196
|
+
run = await self.get_run(run_id)
|
|
197
|
+
if run.failed:
|
|
198
|
+
raise RunFailedError(run.run_id, run.error)
|
|
199
|
+
if run.done:
|
|
200
|
+
return run
|
|
201
|
+
if time.monotonic() >= deadline:
|
|
202
|
+
raise WaitTimeout(f"run {run_id} not done after {timeout:.0f}s")
|
|
203
|
+
await asyncio.sleep(interval)
|
|
204
|
+
|
|
205
|
+
async def analyze(self, file: Any, *, creative_id: str | None = None, with_audio: bool = False,
|
|
206
|
+
audience: str | None = None, filename: str | None = None, wait: bool = True,
|
|
207
|
+
interval: float = 5.0, timeout: float = 1800.0) -> Run:
|
|
208
|
+
video_id = await self.upload_video(file, filename=filename)
|
|
209
|
+
run = await self.create_run(video_id, creative_id=creative_id,
|
|
210
|
+
with_audio=with_audio, audience=audience)
|
|
211
|
+
if wait:
|
|
212
|
+
return await self.wait(run.run_id, interval=interval, timeout=timeout)
|
|
213
|
+
return run
|
creacortex/errors.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Exceptions raised by the Creacortex client."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CreacortexError(Exception):
|
|
10
|
+
"""Base class for every error this library raises."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ApiError(CreacortexError):
|
|
14
|
+
"""An HTTP error returned by the API (status >= 400)."""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self, status_code: int, message: str, *, response: httpx.Response | None = None,
|
|
18
|
+
) -> None:
|
|
19
|
+
super().__init__(f"[{status_code}] {message}")
|
|
20
|
+
self.status_code = status_code
|
|
21
|
+
self.message = message
|
|
22
|
+
self.response = response
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AuthError(ApiError):
|
|
26
|
+
"""Invalid or missing API key (401)."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class InsufficientCreditsError(ApiError):
|
|
30
|
+
"""The project does not have enough credits to run the analysis (402)."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class RateLimitError(ApiError):
|
|
34
|
+
"""Too many requests — slow down (429)."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class NotFoundError(ApiError):
|
|
38
|
+
"""The requested run (or resource) does not exist (404)."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class RunFailedError(CreacortexError):
|
|
42
|
+
"""The analysis run finished with status='failed'."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, run_id: str, error: str | None) -> None:
|
|
45
|
+
super().__init__(f"run {run_id} failed: {error or 'unknown error'}")
|
|
46
|
+
self.run_id = run_id
|
|
47
|
+
self.error = error
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class WaitTimeout(CreacortexError):
|
|
51
|
+
"""A run did not reach a terminal state within the wait timeout."""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
_STATUS_ERRORS: dict[int, type[ApiError]] = {
|
|
55
|
+
401: AuthError,
|
|
56
|
+
402: InsufficientCreditsError,
|
|
57
|
+
404: NotFoundError,
|
|
58
|
+
429: RateLimitError,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def raise_for_status(resp: httpx.Response) -> None:
|
|
63
|
+
"""Map an error response to the appropriate typed exception."""
|
|
64
|
+
if resp.status_code < 400:
|
|
65
|
+
return
|
|
66
|
+
detail = _detail(resp)
|
|
67
|
+
cls = _STATUS_ERRORS.get(resp.status_code, ApiError)
|
|
68
|
+
raise cls(resp.status_code, detail, response=resp)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _detail(resp: httpx.Response) -> str:
|
|
72
|
+
try:
|
|
73
|
+
body: Any = resp.json()
|
|
74
|
+
except Exception: # noqa: BLE001 — non-JSON error body
|
|
75
|
+
return resp.text or resp.reason_phrase
|
|
76
|
+
if isinstance(body, dict) and "detail" in body:
|
|
77
|
+
return str(body["detail"])
|
|
78
|
+
return str(body)
|
creacortex/models.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Typed response models. Plain dataclasses built from the API's JSON."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
# Run lifecycle states. `done` and `failed` are terminal.
|
|
9
|
+
QUEUED = "queued"
|
|
10
|
+
RUNNING = "running"
|
|
11
|
+
DONE = "done"
|
|
12
|
+
FAILED = "failed"
|
|
13
|
+
TERMINAL = frozenset({DONE, FAILED})
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class Run:
|
|
18
|
+
"""An analysis run and the URLs to its results once ready."""
|
|
19
|
+
|
|
20
|
+
run_id: str
|
|
21
|
+
status: str
|
|
22
|
+
creative_id: str | None = None
|
|
23
|
+
with_audio: bool = False
|
|
24
|
+
error: str | None = None
|
|
25
|
+
ready: bool = False
|
|
26
|
+
csv_url: str | None = None
|
|
27
|
+
export_url: str | None = None
|
|
28
|
+
diagnose_pdf_url: str | None = None
|
|
29
|
+
analyze_pdf_url: str | None = None
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def done(self) -> bool:
|
|
33
|
+
return self.status == DONE
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def failed(self) -> bool:
|
|
37
|
+
return self.status == FAILED
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def terminal(self) -> bool:
|
|
41
|
+
return self.status in TERMINAL
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def from_api(cls, d: dict[str, Any]) -> Run:
|
|
45
|
+
return cls(
|
|
46
|
+
run_id=d["run_id"],
|
|
47
|
+
status=d["status"],
|
|
48
|
+
creative_id=d.get("creative_id"),
|
|
49
|
+
with_audio=bool(d.get("with_audio", False)),
|
|
50
|
+
error=d.get("error"),
|
|
51
|
+
ready=bool(d.get("ready", False)),
|
|
52
|
+
csv_url=d.get("csv_url"),
|
|
53
|
+
export_url=d.get("export_url"),
|
|
54
|
+
diagnose_pdf_url=d.get("diagnose_pdf_url"),
|
|
55
|
+
analyze_pdf_url=d.get("analyze_pdf_url"),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class RunListItem:
|
|
61
|
+
"""A run as returned by `list_runs` (lighter than `Run`)."""
|
|
62
|
+
|
|
63
|
+
run_id: str
|
|
64
|
+
status: str
|
|
65
|
+
creative_id: str | None
|
|
66
|
+
with_audio: bool
|
|
67
|
+
ready: bool
|
|
68
|
+
created_at: datetime
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def from_api(cls, d: dict[str, Any]) -> RunListItem:
|
|
72
|
+
return cls(
|
|
73
|
+
run_id=d["run_id"],
|
|
74
|
+
status=d["status"],
|
|
75
|
+
creative_id=d.get("creative_id"),
|
|
76
|
+
with_audio=bool(d.get("with_audio", False)),
|
|
77
|
+
ready=bool(d.get("ready", False)),
|
|
78
|
+
created_at=datetime.fromisoformat(d["created_at"]),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass(frozen=True)
|
|
83
|
+
class Account:
|
|
84
|
+
"""Credit balance for the API key's project."""
|
|
85
|
+
|
|
86
|
+
project: str | None
|
|
87
|
+
balance: float
|
|
88
|
+
quota: float
|
|
89
|
+
used: float
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def from_api(cls, d: dict[str, Any]) -> Account:
|
|
93
|
+
return cls(
|
|
94
|
+
project=d.get("project"),
|
|
95
|
+
balance=float(d["balance"]),
|
|
96
|
+
quota=float(d["quota"]),
|
|
97
|
+
used=float(d.get("used", 0.0)),
|
|
98
|
+
)
|
creacortex/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: creacortex
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for the Creacortex video-creative attention-analysis API
|
|
5
|
+
Project-URL: Homepage, https://creacortex.ai
|
|
6
|
+
Author: Creacortex
|
|
7
|
+
License: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: advertising,analytics,api,attention,creacortex,video
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Requires-Dist: httpx>=0.24
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: mypy>=1.8; extra == 'dev'
|
|
18
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
19
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
20
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# creacortex
|
|
24
|
+
|
|
25
|
+
Python client for the **Creacortex** video-creative attention-analysis API.
|
|
26
|
+
|
|
27
|
+
Upload a video creative, run it through the pipeline, and pull back per-second
|
|
28
|
+
attention data, a Gen-AI optimization payload, and PDF reports.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install creacortex
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Quick start
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from creacortex import Client
|
|
40
|
+
|
|
41
|
+
with Client("ace_your_api_key") as cx:
|
|
42
|
+
# one call: upload + start + wait for completion
|
|
43
|
+
run = cx.analyze("ad.mp4", with_audio=True, creative_id="summer-promo")
|
|
44
|
+
|
|
45
|
+
csv = cx.get_csv(run.run_id) # per-second analysis (bytes)
|
|
46
|
+
payload = cx.get_export(run.run_id) # Gen-AI insights (dict)
|
|
47
|
+
pdf = cx.get_report(run.run_id, "diagnose") # PDF report (bytes)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Get an API key from **Usage & billing → API keys** in the dashboard. It is sent
|
|
51
|
+
as the `X-API-Key` header.
|
|
52
|
+
|
|
53
|
+
## Async
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
import asyncio
|
|
57
|
+
from creacortex import AsyncClient
|
|
58
|
+
|
|
59
|
+
async def main():
|
|
60
|
+
async with AsyncClient("ace_your_api_key") as cx:
|
|
61
|
+
run = await cx.analyze("ad.mp4")
|
|
62
|
+
print(await cx.get_export(run.run_id))
|
|
63
|
+
|
|
64
|
+
asyncio.run(main())
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Step by step
|
|
68
|
+
|
|
69
|
+
`analyze()` is a convenience over the raw endpoints, which you can also call directly:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
video_id = cx.upload_video("ad.mp4")
|
|
73
|
+
run = cx.create_run(video_id, with_audio=True, audience="men 20-45, mobile gamers")
|
|
74
|
+
run = cx.wait(run.run_id) # poll until done (or RunFailedError / WaitTimeout)
|
|
75
|
+
|
|
76
|
+
for item in cx.list_runs():
|
|
77
|
+
print(item.run_id, item.status, item.ready)
|
|
78
|
+
|
|
79
|
+
print(cx.get_account().balance) # credits left
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`upload_video` / `analyze` accept a file path, raw `bytes`, or a binary file object.
|
|
83
|
+
|
|
84
|
+
## Errors
|
|
85
|
+
|
|
86
|
+
| Exception | When |
|
|
87
|
+
|---|---|
|
|
88
|
+
| `AuthError` | invalid/missing API key (401) |
|
|
89
|
+
| `InsufficientCreditsError` | not enough credits (402) |
|
|
90
|
+
| `RateLimitError` | too many requests (429) |
|
|
91
|
+
| `NotFoundError` | unknown run (404) |
|
|
92
|
+
| `RunFailedError` | the run finished with `status="failed"` |
|
|
93
|
+
| `WaitTimeout` | `wait()` exceeded its timeout |
|
|
94
|
+
| `ApiError` | any other HTTP error (has `.status_code`) |
|
|
95
|
+
|
|
96
|
+
All inherit from `CreacortexError`.
|
|
97
|
+
|
|
98
|
+
## Configuration
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
Client("ace_...", base_url="https://api.creacortex.ai", timeout=60.0)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
MIT
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
creacortex/__init__.py,sha256=MYqy4Y_EWDtllOiihSszSdv78J6AANTU_rs3aIydFNQ,892
|
|
2
|
+
creacortex/client.py,sha256=k1Iplv09wEg0Qvd8fPVztJJDij5oWZFTN9u0hA4QJFA,8801
|
|
3
|
+
creacortex/errors.py,sha256=OE8pp0JDmXsBz-ZAj9t6_x8mf5kBR_PSlfYu1vuDECE,2139
|
|
4
|
+
creacortex/models.py,sha256=Ju39mSXxWlQO3sQ_Fxw74mbOknN5lVcvyyBmbg5Z8RE,2590
|
|
5
|
+
creacortex/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
creacortex-0.1.0.dist-info/METADATA,sha256=1YoL3rxP49FCJDMVORljMA99oPREAI0l2gN1USEMCjo,2994
|
|
7
|
+
creacortex-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
8
|
+
creacortex-0.1.0.dist-info/licenses/LICENSE,sha256=Srvb_fua232oRVeo451wZ9Fp0EVZmdLcCKfPwt91zjc,1067
|
|
9
|
+
creacortex-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Creacortex
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|