mnml-ai 0.1.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.
- mnml_ai-0.1.0/.gitignore +12 -0
- mnml_ai-0.1.0/CHANGELOG.md +44 -0
- mnml_ai-0.1.0/LICENSE +21 -0
- mnml_ai-0.1.0/PKG-INFO +338 -0
- mnml_ai-0.1.0/README.md +305 -0
- mnml_ai-0.1.0/pyproject.toml +66 -0
- mnml_ai-0.1.0/spec/openapi.json +7428 -0
- mnml_ai-0.1.0/src/mnml_ai/__init__.py +18 -0
- mnml_ai-0.1.0/src/mnml_ai/_client.py +670 -0
- mnml_ai-0.1.0/src/mnml_ai/_errors.py +42 -0
- mnml_ai-0.1.0/src/mnml_ai/py.typed +0 -0
- mnml_ai-0.1.0/src/mnml_ai/types.py +297 -0
- mnml_ai-0.1.0/src/mnml_ai/webhooks.py +68 -0
- mnml_ai-0.1.0/tests/__init__.py +0 -0
- mnml_ai-0.1.0/tests/conftest.py +64 -0
- mnml_ai-0.1.0/tests/test_client.py +361 -0
- mnml_ai-0.1.0/tests/test_spec.py +167 -0
- mnml_ai-0.1.0/tests/test_webhooks.py +76 -0
mnml_ai-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this package are recorded here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions follow
|
|
5
|
+
[Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## 0.1.0 — unreleased
|
|
8
|
+
|
|
9
|
+
First release.
|
|
10
|
+
|
|
11
|
+
- Speaks API v2: every call is under `/v2` (`/v1` is now the mnmlai.dev API, which the SDK
|
|
12
|
+
does not call). `uploads.create`, `upload_id` and `mask_upload_id` are gone, as the API no
|
|
13
|
+
longer has uploads: send the image in the call, or a finished `job_id`.
|
|
14
|
+
- A create call's `status` is typed `JobStatus`, as the API now promises (it once answered
|
|
15
|
+
the raw stored status on an idempotent replay). `spec/openapi.json` refreshed (Oct 18 2026).
|
|
16
|
+
- `jobs.stream(job_id)`: follow a job over one connection (Server-Sent Events), as a generator of
|
|
17
|
+
`JobStreamEvent`s: `job` as it changes, then `done`, `timeout` or `error`. `Mnml` takes a
|
|
18
|
+
`stream_transport` for it, as it takes `transport`.
|
|
19
|
+
|
|
20
|
+
- `image` on `renders`, `edits`, `enhancements` and `videos`: send the image in the call, with no
|
|
21
|
+
upload step. A `str` (a link, a data URI or base64) is sent as it is; bytes, a `Path` or an open
|
|
22
|
+
binary file is sent as a base64 data URI, as JSON.
|
|
23
|
+
- `references` items and `end_frame` take an image the same way, or a dict with `image`.
|
|
24
|
+
- `mask` on `edits`: a PNG mask in the call, white to change and black to keep.
|
|
25
|
+
- `image_url` still works and is deprecated: it is the old name of `image`.
|
|
26
|
+
- `wait` (1-90 seconds) on `renders.create` and `jobs.get`: the API holds the answer until the job
|
|
27
|
+
settles. `create_and_wait` and `jobs.wait` use it, so a render is one or two calls, not a polling
|
|
28
|
+
loop.
|
|
29
|
+
- `Mnml` client for the mnml API v2: renders, edits, enhancements, videos, jobs
|
|
30
|
+
(`get`, `wait`, `wait_all`, `cancel`), account and engines.
|
|
31
|
+
- Every request field as a typed keyword argument; a field the API does not take is a `TypeError`.
|
|
32
|
+
- `create_and_wait` on renders, edits, enhancements and videos: start a job and wait for it.
|
|
33
|
+
- `files.download(output, to=None)`: a job output's bytes and content type, from its signed link,
|
|
34
|
+
without sending the key.
|
|
35
|
+
- `TypedDict`s for every answer, the nested ones included (engine prices and capabilities, video
|
|
36
|
+
prices, account limits), and `Literal`s for the values the API takes: engine and video model ids,
|
|
37
|
+
camera moves, motion, modes, aspect ratios.
|
|
38
|
+
- `WebhookEvent` as a union that narrows `data` on `type` (`job.*`, `credits.low`, `webhook.test`).
|
|
39
|
+
- A migration table from the mnmlai.dev routes to SDK calls.
|
|
40
|
+
- Retries for 429, 5xx and dropped connections, with `Retry-After` up to a minute.
|
|
41
|
+
- An `Idempotency-Key` on every spending call, kept across retries.
|
|
42
|
+
- `MnmlError` with the API's `code`, `status`, `request_id` and validation `issues`.
|
|
43
|
+
- `verify_webhook` and `sign_webhook` (Standard Webhooks).
|
|
44
|
+
- Typed answers in `mnml_ai.types`, and `py.typed`. No dependencies, Python 3.9+.
|
mnml_ai-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mnml
|
|
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.
|
mnml_ai-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: mnml-ai
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The official Python client for the mnml API: architecture renders, edits, enhancements and video.
|
|
5
|
+
Project-URL: Homepage, https://developers.mnml.ai/sdks
|
|
6
|
+
Project-URL: Documentation, https://developers.mnml.ai/docs
|
|
7
|
+
Project-URL: Source, https://github.com/mnml-ai/mnml-sdk-python
|
|
8
|
+
Project-URL: Issues, https://github.com/mnml-ai/mnml-sdk-python/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/mnml-ai/mnml-sdk-python/blob/main/CHANGELOG.md
|
|
10
|
+
Author-email: mnml <hello@mnml.ai>
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: ai,api,architecture,mnml,rendering,sdk
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.9
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
29
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
31
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# mnml Python SDK
|
|
35
|
+
|
|
36
|
+
The official Python client for the [mnml API](https://developers.mnml.ai): architecture
|
|
37
|
+
renders, edits, enhancements and video, from your own product.
|
|
38
|
+
|
|
39
|
+
- Every endpoint of API v2: renders, edits, enhancements, video, jobs, account, engines
|
|
40
|
+
- Images sent in the call itself: a link, bytes, a `Path` or an open file, no upload step
|
|
41
|
+
- Retries, timeouts and idempotency keys handled for you
|
|
42
|
+
- `create_and_wait()` and `jobs.wait()` to poll a job until it settles, `files.download()` to keep
|
|
43
|
+
its output
|
|
44
|
+
- Webhook signature verification, with typed events
|
|
45
|
+
- Typed keyword arguments and answers (`TypedDict`), checked against the API's OpenAPI document
|
|
46
|
+
- No dependencies. Python 3.9+
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install mnml-ai
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Quick start
|
|
53
|
+
|
|
54
|
+
Create an API key in the [console](https://developers.mnml.ai/console/keys), then:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from pathlib import Path
|
|
58
|
+
|
|
59
|
+
from mnml_ai import Mnml
|
|
60
|
+
|
|
61
|
+
mnml = Mnml() # reads MNML_API_KEY
|
|
62
|
+
|
|
63
|
+
job = mnml.renders.create_and_wait(
|
|
64
|
+
mode="exterior",
|
|
65
|
+
image=Path("massing.png"), # or a link: "https://example.com/massing.png"
|
|
66
|
+
prompt="Timber facade, late afternoon light, olive trees",
|
|
67
|
+
)[0]
|
|
68
|
+
print(job["status"], job["outputs"][0]["url"] if job["outputs"] else None)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Every call spends credits from your prepaid API balance. Top it up in dollars on
|
|
72
|
+
[Billing](https://developers.mnml.ai/console/billing).
|
|
73
|
+
|
|
74
|
+
## Configuration
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
mnml = Mnml(
|
|
78
|
+
api_key=None, # defaults to the MNML_API_KEY environment variable
|
|
79
|
+
max_retries=2, # retries for a 429, a 5xx or a dropped connection
|
|
80
|
+
timeout=60.0, # seconds, per request
|
|
81
|
+
)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Methods
|
|
85
|
+
|
|
86
|
+
| Method | Endpoint | What it does |
|
|
87
|
+
| --------------------------------- | --------------------------- | --------------------------------------------------- |
|
|
88
|
+
| `renders.create(...)` | `POST /v2/renders` | Render from an image, or from a prompt alone |
|
|
89
|
+
| `edits.create(...)` | `POST /v2/edits` | Edit or erase, over the whole image or a region |
|
|
90
|
+
| `enhancements.create(...)` | `POST /v2/enhancements` | Upscale, enhance, remove the background, outpaint |
|
|
91
|
+
| `videos.create(...)` | `POST /v2/videos` | Video from a still |
|
|
92
|
+
| `jobs.get(id)` | `GET /v2/jobs/{id}` | Read a job |
|
|
93
|
+
| `jobs.cancel(id)` | `POST /v2/jobs/{id}/cancel` | Cancel a job (refunded if it had not produced yet) |
|
|
94
|
+
| `account.get()` | `GET /v2/account` | Balance, tier and limits for this key |
|
|
95
|
+
| `engines.list()` | `GET /v2/engines` | Engines, video models, prices and capabilities |
|
|
96
|
+
| `files.download(output, to=None)` | `GET /v2/files/{id}` | A job output's bytes, from its signed link |
|
|
97
|
+
| `jobs.wait(id, ...)` | `GET /v2/jobs/{id}` | Read a job until it succeeds, fails or is cancelled |
|
|
98
|
+
| `jobs.wait_all(ids, ...)` | `GET /v2/jobs/{id}` | `wait` for several jobs at once |
|
|
99
|
+
| `jobs.stream(id)` | `GET /v2/jobs/{id}/events` | Follow a job as it changes, over one connection |
|
|
100
|
+
| `<resource>.create_and_wait(...)` | the create, then the job | Start a job and wait for it (a render: every job) |
|
|
101
|
+
|
|
102
|
+
Create calls take the API's fields as keyword arguments, exactly as the
|
|
103
|
+
[reference](https://developers.mnml.ai/docs/renders) names them, typed so your editor completes
|
|
104
|
+
them, and every call returns the answer's `data` as a `dict`. A field the API does not take is a
|
|
105
|
+
`TypeError` before anything is sent. `create_and_wait` is on `renders`, `edits`, `enhancements` and
|
|
106
|
+
`videos`; a render returns a list with one job per `count`, the others one job.
|
|
107
|
+
|
|
108
|
+
### Engines, modes and settings
|
|
109
|
+
|
|
110
|
+
`engine`, `mode`, `aspect_ratio`, `camera_movement`, `motion` and the video `model` are typed with
|
|
111
|
+
the values the API takes. `engines.list()` is the live list, with each engine's price and what it
|
|
112
|
+
can do:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
for engine in mnml.engines.list()["engines"]:
|
|
116
|
+
print(engine["id"], engine["prices"]["render"], engine["capabilities"]["max_references"])
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Images
|
|
120
|
+
|
|
121
|
+
Send the image in the call, as `image`:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from pathlib import Path
|
|
125
|
+
|
|
126
|
+
mnml.renders.create(image="https://example.com/plan.png", prompt="Brick and glass, dusk")
|
|
127
|
+
mnml.renders.create(image=Path("plan.png"), prompt="Brick and glass, dusk")
|
|
128
|
+
mnml.renders.create(image=png_bytes, prompt="Brick and glass, dusk")
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
A `str` is sent as it is: a public `https://` link, a `data:image/...;base64,` URI or bare base64.
|
|
132
|
+
Bytes, a `Path` or an open binary file is read and sent as a data URI, up to 15 MB per image. A
|
|
133
|
+
`str` is never read as a file, so pass `Path("plan.png")`, not `"plan.png"`. `image_url` is the
|
|
134
|
+
old name of `image` and still works.
|
|
135
|
+
|
|
136
|
+
Every place an image goes takes the same: `image`, each of `references`, an edit's `mask` and a
|
|
137
|
+
video's `end_frame`. A reference can also be a dict with a `mode`:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
mnml.renders.create(
|
|
141
|
+
image=Path("massing.png"),
|
|
142
|
+
references=[Path("timber.jpg"), {"image": "https://example.com/brick.png", "mode": "material"}],
|
|
143
|
+
prompt="Timber and brick, overcast",
|
|
144
|
+
)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
To work on a finished job's output without downloading it, pass its `job_id`:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
mnml.edits.create(job_id=job["id"], prompt="Dark brick instead of render")
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Edits, enhancements and video
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
# Change one thing, inside a box (fractions of the image from its top-left)
|
|
157
|
+
mnml.edits.create(
|
|
158
|
+
job_id=job_id, prompt="A red front door", region={"box": {"x": 0.4, "y": 0.5, "width": 0.2, "height": 0.4}}
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Or paint a mask: a PNG, white to change and black to keep
|
|
162
|
+
mnml.edits.create(image=Path("house.jpg"), mask=Path("door-mask.png"), prompt="A red front door")
|
|
163
|
+
|
|
164
|
+
# Remove what a region covers
|
|
165
|
+
mnml.edits.create(job_id=job_id, kind="erase", region={"box": {"x": 0.1, "y": 0.6, "width": 0.2, "height": 0.3}})
|
|
166
|
+
|
|
167
|
+
# Upscale, enhance, cut out, or extend to a new frame
|
|
168
|
+
mnml.enhancements.create(job_id=job_id, kind="outpaint", aspect_ratio="16:9")
|
|
169
|
+
|
|
170
|
+
# A ten-second camera move
|
|
171
|
+
mnml.videos.create(job_id=job_id, model="v2.0-flash", duration_seconds=10, camera_movement="orbit-right")
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Waiting for a job
|
|
175
|
+
|
|
176
|
+
A render can come back finished from the create call itself: pass `wait` (1-90 seconds) and the
|
|
177
|
+
answer's `jobs` carry the outputs. A read takes `wait` too, and answers as soon as the job settles.
|
|
178
|
+
`create_and_wait` and `jobs.wait` use both, so a render usually takes one or two calls, not a
|
|
179
|
+
polling loop.
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
started = mnml.renders.create(image=Path("massing.png"), prompt="Brick, dusk", wait=90)
|
|
183
|
+
print(started.get("jobs", [{}])[0].get("outputs")) # set when it finished in time
|
|
184
|
+
|
|
185
|
+
read = mnml.jobs.get(started["id"], wait=90) # held until it settles
|
|
186
|
+
|
|
187
|
+
job = mnml.jobs.wait(job_id, interval=3.0, timeout=600.0)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
`jobs.wait` raises `MnmlTimeoutError` when its own time runs out. The job keeps running, so
|
|
191
|
+
read it again later. `videos.create_and_wait` reads every 10 seconds for up to 20 minutes unless
|
|
192
|
+
you say otherwise. For long jobs such as video, a [webhook](#webhooks) beats polling.
|
|
193
|
+
|
|
194
|
+
### Following a job as it runs
|
|
195
|
+
|
|
196
|
+
`jobs.stream` follows one job over a single connection (Server-Sent Events) for up to ten
|
|
197
|
+
minutes: each change of the job, then one last event, after which the loop ends. It suits a long
|
|
198
|
+
job in a script with no server to take a webhook.
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
for event in mnml.jobs.stream(job_id):
|
|
202
|
+
if event["type"] == "job":
|
|
203
|
+
print(event["job"]["status"]) # as it changes
|
|
204
|
+
elif event["type"] == "done":
|
|
205
|
+
print(event["job"]["outputs"][0]["url"]) # settled
|
|
206
|
+
elif event["type"] == "timeout":
|
|
207
|
+
break # still running after ten minutes: stream it again
|
|
208
|
+
else:
|
|
209
|
+
print(event["error"]["message"]) # the API could not read it; the job still runs
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Leaving the loop hangs up; the job keeps running. A connection that drops before the last event
|
|
213
|
+
raises `MnmlError` with `STREAM_ENDED`.
|
|
214
|
+
|
|
215
|
+
### Downloading outputs
|
|
216
|
+
|
|
217
|
+
Output links are signed and expire after an hour or two. Download what you want to keep:
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
file = mnml.files.download(job["outputs"][0], to="render.jpg")
|
|
221
|
+
print(file["content_type"], len(file["data"]))
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
The link's signature is its credential, so your key is not sent with it. An expired link raises
|
|
225
|
+
`MnmlError` with `NOT_FOUND`; read the job again for fresh links.
|
|
226
|
+
|
|
227
|
+
## Errors
|
|
228
|
+
|
|
229
|
+
A refusal raises `MnmlError`, carrying the API's `code`, the HTTP `status` and the
|
|
230
|
+
`request_id` to quote to support:
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
from mnml_ai import MnmlError
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
mnml.renders.create(image=url, prompt=prompt)
|
|
237
|
+
except MnmlError as err:
|
|
238
|
+
if err.code == "INSUFFICIENT_CREDITS":
|
|
239
|
+
... # top up, then send it again
|
|
240
|
+
raise
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
On `VALIDATION_FAILED`, `err.issues` lists each refused field. Every code is described at
|
|
244
|
+
[developers.mnml.ai/docs/errors](https://developers.mnml.ai/docs/errors).
|
|
245
|
+
|
|
246
|
+
## Retries and idempotency
|
|
247
|
+
|
|
248
|
+
The client retries a `429`, a `5xx` or a dropped connection, twice by default. It waits out
|
|
249
|
+
`Retry-After` when that is a minute or less, and otherwise raises the error to you. That
|
|
250
|
+
covers a daily limit, which resets at midnight UTC.
|
|
251
|
+
|
|
252
|
+
Every call that can spend credits carries an `Idempotency-Key`, and the client reuses the same
|
|
253
|
+
key across its own retries, so a retry never charges twice. To make your own retries safe as
|
|
254
|
+
well, pass a key you choose:
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
mnml.renders.create(prompt=prompt, image=url, idempotency_key=f"order-{order_id}")
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Webhooks
|
|
261
|
+
|
|
262
|
+
Add an endpoint and copy its signing secret on the
|
|
263
|
+
[Webhooks](https://developers.mnml.ai/console/webhooks) page. Then verify each delivery with
|
|
264
|
+
the raw request body, before parsing it:
|
|
265
|
+
|
|
266
|
+
```python
|
|
267
|
+
import os
|
|
268
|
+
from flask import Flask, request
|
|
269
|
+
from mnml_ai import WebhookVerificationError, verify_webhook
|
|
270
|
+
|
|
271
|
+
app = Flask(__name__)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
@app.post("/webhooks/mnml")
|
|
275
|
+
def mnml_webhook():
|
|
276
|
+
try:
|
|
277
|
+
event = verify_webhook(request.get_data(), request.headers, os.environ["MNML_WEBHOOK_SECRET"])
|
|
278
|
+
except WebhookVerificationError:
|
|
279
|
+
return "", 400
|
|
280
|
+
if event["type"] == "job.succeeded":
|
|
281
|
+
job = event["data"] # the job, as jobs.get returns it
|
|
282
|
+
elif event["type"] == "credits.low":
|
|
283
|
+
balance = event["data"]["balance"] # and event["data"]["threshold"]
|
|
284
|
+
return "", 204
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
`verify_webhook` raises `WebhookVerificationError` on a bad signature or a stale timestamp
|
|
288
|
+
(over five minutes). Events: `job.succeeded`, `job.failed`, `job.canceled`, `credits.low` and
|
|
289
|
+
`webhook.test`; checking `event["type"]` narrows `event["data"]` for a type checker. The scheme is
|
|
290
|
+
[Standard Webhooks](https://www.standardwebhooks.com). See
|
|
291
|
+
[`examples/`](./examples) for FastAPI and Django.
|
|
292
|
+
|
|
293
|
+
## Typing
|
|
294
|
+
|
|
295
|
+
The answers are typed as `TypedDict`s in `mnml_ai.types` (`Job`, `RenderStarted`, `JobStreamEvent`,
|
|
296
|
+
`Account`, `Engines`, `WebhookEvent` and more), the values the API takes as `Literal`s (`Mode`,
|
|
297
|
+
`EngineId`, `CameraMove` …), and the package ships `py.typed`. A test in this repository checks
|
|
298
|
+
every keyword argument, field and listed value against the API's OpenAPI document
|
|
299
|
+
([`spec/openapi.json`](./spec/openapi.json)).
|
|
300
|
+
|
|
301
|
+
## Moving from the mnmlai.dev API
|
|
302
|
+
|
|
303
|
+
The mnmlai.dev routes (`/v1/archDiffusion-v46`, `/v1/upscale`, `/v1/status/{id}` …) still answer
|
|
304
|
+
at `/v1` on `api.mnml.ai` and on `api.mnmlai.dev`, deprecated, so nothing breaks while you move.
|
|
305
|
+
The SDK speaks API v2 only. Each old route has a v2 call that does the same job:
|
|
306
|
+
|
|
307
|
+
| mnmlai.dev route | SDK call |
|
|
308
|
+
| ------------------------------------------------------------- | ------------------------------------------------------------- |
|
|
309
|
+
| `archDiffusion-v46` | `renders.create(engine="v4.6-ultra", ...)` |
|
|
310
|
+
| `archDiffusion-v45`, `-v45-lite` | `renders.create(engine="v4.5-ultra")`, `"v4.5-fast"` |
|
|
311
|
+
| `archDiffusion-v44`, `-v44-lite` | `renders.create(engine="v4.4-ultra")`, `"v4.4-fast"` |
|
|
312
|
+
| `archDiffusion-v43`, `-v43-lite`, `-v42`, `-v42-lite`, `-v41` | `renders.create(engine="v4.3")`, `"v4.3-fast"` |
|
|
313
|
+
| `mixture-of-experts` | `renders.create(mode=..., ...)` (`expert_name` is the `mode`) |
|
|
314
|
+
| `exterior`, `interior`, `sketch-to-img` | `renders.create(engine="v3.1", mode=..., ...)` |
|
|
315
|
+
| `style/transfer` | `renders.create(references=[{..., "mode": "style"}], ...)` |
|
|
316
|
+
| `imagine-ai` | `renders.create(mode="text-to-render", prompt=...)` |
|
|
317
|
+
| `virtual-staging-ai` (v1 and v2) | `renders.create(mode="interior", ...)` |
|
|
318
|
+
| `inpaint` | `edits.create(prompt=..., mask=...)` or `region` |
|
|
319
|
+
| `ai-eraser` | `edits.create(kind="erase", region=...)` |
|
|
320
|
+
| `upscale`, `render/enhancer` | `enhancements.create(kind="upscale")`, `kind="enhance"` |
|
|
321
|
+
| `video-v20-flash`, `video-v20-cinematic`, `video-ai` | `videos.create(model="v2.0-flash")`, `"v2.0"`, `"v1.1"` |
|
|
322
|
+
| `status/{id}` (v1 and v2) | `jobs.get(id)` or `jobs.wait(id)` |
|
|
323
|
+
| `credits` | `account.get()` |
|
|
324
|
+
|
|
325
|
+
v2 takes the image in the request, as mnmlai.dev did: pass it as `image` (a link, bytes or a
|
|
326
|
+
`Path`), or a `job_id` for a finished job's output. The full guide is at
|
|
327
|
+
[developers.mnml.ai/docs/migrate](https://developers.mnml.ai/docs/migrate).
|
|
328
|
+
|
|
329
|
+
## Links
|
|
330
|
+
|
|
331
|
+
- [Documentation](https://developers.mnml.ai/docs)
|
|
332
|
+
- [API reference](https://developers.mnml.ai/docs/renders)
|
|
333
|
+
- [TypeScript SDK](https://github.com/mnml-ai/mnml-sdk-typescript)
|
|
334
|
+
- [Changelog](./CHANGELOG.md)
|
|
335
|
+
|
|
336
|
+
## License
|
|
337
|
+
|
|
338
|
+
[MIT](./LICENSE)
|