llamagen-python 0.1.3__tar.gz → 0.1.6__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 (22) hide show
  1. {llamagen_python-0.1.3 → llamagen_python-0.1.6}/.gitignore +1 -0
  2. llamagen_python-0.1.6/PKG-INFO +349 -0
  3. llamagen_python-0.1.6/README.md +324 -0
  4. llamagen_python-0.1.6/examples/quickstart.py +246 -0
  5. {llamagen_python-0.1.3 → llamagen_python-0.1.6}/pyproject.toml +1 -1
  6. {llamagen_python-0.1.3 → llamagen_python-0.1.6}/src/llamagen/__init__.py +1 -1
  7. {llamagen_python-0.1.3 → llamagen_python-0.1.6}/src/llamagen/_http.py +6 -2
  8. {llamagen_python-0.1.3 → llamagen_python-0.1.6}/src/llamagen/resources/animations.py +12 -4
  9. {llamagen_python-0.1.3 → llamagen_python-0.1.6}/src/llamagen/resources/comics.py +13 -5
  10. {llamagen_python-0.1.3 → llamagen_python-0.1.6}/tests/test_client.py +42 -1
  11. llamagen_python-0.1.3/PKG-INFO +0 -186
  12. llamagen_python-0.1.3/README.md +0 -161
  13. llamagen_python-0.1.3/examples/quickstart.py +0 -23
  14. {llamagen_python-0.1.3 → llamagen_python-0.1.6}/LICENSE +0 -0
  15. {llamagen_python-0.1.3 → llamagen_python-0.1.6}/docs/RELEASE.md +0 -0
  16. {llamagen_python-0.1.3 → llamagen_python-0.1.6}/src/llamagen/_client.py +0 -0
  17. {llamagen_python-0.1.3 → llamagen_python-0.1.6}/src/llamagen/_errors.py +0 -0
  18. {llamagen_python-0.1.3 → llamagen_python-0.1.6}/src/llamagen/_types.py +0 -0
  19. {llamagen_python-0.1.3 → llamagen_python-0.1.6}/src/llamagen/_webhooks.py +0 -0
  20. {llamagen_python-0.1.3 → llamagen_python-0.1.6}/src/llamagen/py.typed +0 -0
  21. {llamagen_python-0.1.3 → llamagen_python-0.1.6}/src/llamagen/resources/__init__.py +0 -0
  22. {llamagen_python-0.1.3 → llamagen_python-0.1.6}/tests/test_webhooks.py +0 -0
@@ -6,6 +6,7 @@
6
6
  /.ruff_cache/
7
7
  /.mypy_cache/
8
8
  /.venv/
9
+ /.env
9
10
  __pycache__/
10
11
  .ipynb_checkpoints/
11
12
  *.py[cod]
@@ -0,0 +1,349 @@
1
+ Metadata-Version: 2.4
2
+ Name: llamagen-python
3
+ Version: 0.1.6
4
+ Summary: Official Python SDK for LlamaGen Comic API and Animation API
5
+ Project-URL: Homepage, https://llamagen.ai/comic-api
6
+ Project-URL: Documentation, https://llamagen.ai/comic-api/docs
7
+ Project-URL: Source, https://github.com/LlamaGenAI/llamagen-python
8
+ Project-URL: Issues, https://github.com/LlamaGenAI/llamagen-python/issues
9
+ Author-email: "llamagen.ai" <support@llamagen.ai>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: ai,ai comic api,animation,comic,image-to-video,llamagen,sdk,text-to-video
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Requires-Python: >=3.9
24
+ Description-Content-Type: text/markdown
25
+
26
+ # llamagen-python
27
+
28
+ Official Python SDK for LlamaGen's Comic API and Animation API.
29
+
30
+ Homepage: <https://llamagen.ai/comic-api>
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ pip install llamagen-python
36
+ ```
37
+
38
+ ## Quick Start
39
+
40
+ ```python
41
+ import os
42
+ from pathlib import Path
43
+
44
+ from llamagen import LlamaGenClient
45
+
46
+ WAIT_TIMEOUT_MS = 30 * 60 * 1000
47
+
48
+ client = LlamaGenClient(
49
+ api_key=os.environ["LLAMAGEN_API_KEY"],
50
+ timeout_ms=30000,
51
+ max_retries=5,
52
+ retry_delay_ms=500,
53
+ )
54
+
55
+ created = client.comic.create({
56
+ "prompt": "A 4-panel comic about Leo finding a glowing key in a quiet library.",
57
+ "style": "manga",
58
+ "fixPanelNum": 4,
59
+ })
60
+ print("comic.create:", created["id"], created.get("status"))
61
+
62
+ done = client.comic.wait_for_completion(created["id"], timeout_ms=WAIT_TIMEOUT_MS)
63
+ print("comic.wait_for_completion:", done["id"], done.get("status"), done.get("comics"))
64
+ ```
65
+
66
+ The full runnable demo is in [`examples/quickstart.py`](examples/quickstart.py).
67
+ It loads `LLAMAGEN_API_KEY` from the environment or a local `.env` file.
68
+ Set `LLAMAGEN_UPLOAD_FILE=/path/to/reference.png` to run the upload demo, and
69
+ set `LLAMAGEN_RUN_OPTIONAL_DEMOS=1` to run extra paid generation examples.
70
+
71
+ ## Client
72
+
73
+ ```python
74
+ client = LlamaGenClient(
75
+ api_key="YOUR_API_KEY",
76
+ base_url="https://api.llamagen.ai/v1",
77
+ timeout_ms=30000,
78
+ max_retries=5,
79
+ retry_delay_ms=500,
80
+ )
81
+ ```
82
+
83
+ The SDK uses Python's standard library HTTP stack and does not require runtime
84
+ dependencies.
85
+
86
+ ## Comic API
87
+
88
+ Use `llamagen.comic` to create comics, manga, webtoons, storyboards,
89
+ consistent-character pages, and panel edits.
90
+
91
+ ```python
92
+ generation = client.comic.create({
93
+ "prompt": "The Little Prince meets the Fox in a luminous desert.",
94
+ "style": "storybook manga",
95
+ "size": "1024x1024",
96
+ "pagination": {"totalPages": 2, "panelsPerPage": 4},
97
+ "comicRoles": [
98
+ {
99
+ "name": "The Little Prince",
100
+ "image": "https://example.com/prince.png",
101
+ "clothing": "green coat and yellow scarf",
102
+ }
103
+ ],
104
+ "attachments": [{"type": "image", "url": "https://example.com/reference.png"}],
105
+ "language": "en",
106
+ "upscale": "2K",
107
+ })
108
+ ```
109
+
110
+ Comic methods:
111
+
112
+ - `llamagen.comic.create(params)` calls `POST /v1/comics/generations`.
113
+ - `llamagen.comic.get(id, page=None, panel=None)` calls `GET /v1/comics/generations/{id}`.
114
+ - `llamagen.comic.continue_write(id, params)` extends an existing comic.
115
+ - `llamagen.comic.update_panel(id, params)` regenerates one panel.
116
+ - `llamagen.comic.usage()` calls `GET /v1/comics/usage`.
117
+ - `llamagen.comic.upload(file, filename=None)` calls `POST /v1/comics/upload`.
118
+ - `llamagen.comic.wait_for_completion(id, **options)` polls until a terminal status.
119
+ - `llamagen.comic.create_and_wait(params, **options)` creates and polls.
120
+ - `llamagen.comic.create_batch(params_list, concurrency=3)` submits many jobs.
121
+ - `llamagen.comic.wait_for_many(ids, concurrency=3)` polls many jobs.
122
+
123
+ Comic examples:
124
+
125
+ ```python
126
+ # Create a comic generation.
127
+ created = client.comic.create({
128
+ "prompt": "A 4-panel comic about Leo finding a glowing key in a quiet library.",
129
+ "style": "manga",
130
+ "fixPanelNum": 4,
131
+ })
132
+ print("comic.create:", created["id"], created.get("status"))
133
+
134
+ # Fetch the whole generation, or one page/panel.
135
+ fetched = client.comic.get(created["id"])
136
+ print("comic.get:", fetched["id"], fetched.get("status"))
137
+
138
+ panel = client.comic.get(created["id"], page=0, panel=0)
139
+ print("comic.get panel:", panel.get("id", created["id"]), panel.get("status"))
140
+
141
+ # Wait until the generation reaches a terminal status.
142
+ # The default timeout is 30 minutes.
143
+ done = client.comic.wait_for_completion(created["id"], timeout_ms=WAIT_TIMEOUT_MS)
144
+ print("comic.wait_for_completion:", done["id"], done.get("status"), done.get("comics"))
145
+
146
+ # Create and wait in one call.
147
+ finished = client.comic.create_and_wait({
148
+ "prompt": "A cozy 3-panel comic about a robot learning to bake bread.",
149
+ "style": "storybook",
150
+ "fixPanelNum": 3,
151
+ }, timeout_ms=WAIT_TIMEOUT_MS)
152
+ print("comic.create_and_wait:", finished["id"], finished.get("status"))
153
+
154
+ # Continue an existing comic.
155
+ continued = client.comic.continue_write(
156
+ created["id"],
157
+ {
158
+ "prompt": "Continue the story with Leo opening a secret reading room.",
159
+ "pagination": {"totalPages": 1, "panelsPerPage": 4},
160
+ },
161
+ )
162
+ print("comic.continue_write:", continued["id"], continued.get("status"))
163
+
164
+ # Regenerate one panel.
165
+ updated = client.comic.update_panel(
166
+ created["id"],
167
+ {
168
+ "panelIndex": 0,
169
+ "prompt": "Make the glowing key brighter and add dramatic moonlight.",
170
+ },
171
+ )
172
+ print("comic.update_panel:", updated["id"], updated.get("status"))
173
+
174
+ # Check usage.
175
+ usage = client.comic.usage()
176
+ print("comic.usage:", usage)
177
+
178
+ # Upload a local reference image.
179
+ upload_file = os.environ.get("LLAMAGEN_UPLOAD_FILE")
180
+ if upload_file:
181
+ uploaded = client.comic.upload(Path(upload_file))
182
+ print("comic.upload:", uploaded)
183
+
184
+ # Submit many jobs and wait for many jobs.
185
+ batch = client.comic.create_batch(
186
+ [
187
+ {
188
+ "prompt": "A 2-panel comic about a cloud trying on hats.",
189
+ "style": "cartoon",
190
+ "fixPanelNum": 2,
191
+ },
192
+ {
193
+ "prompt": "A 2-panel comic about a tiny train crossing a desk.",
194
+ "style": "manga",
195
+ "fixPanelNum": 2,
196
+ },
197
+ ],
198
+ concurrency=2,
199
+ )
200
+ ids = [item["result"]["id"] for item in batch if "result" in item]
201
+ print("comic.create_batch:", ids)
202
+
203
+ finished_many = client.comic.wait_for_many(ids, concurrency=2, timeout_ms=WAIT_TIMEOUT_MS)
204
+ print("comic.wait_for_many:", [(item["id"], item.get("status")) for item in finished_many])
205
+ ```
206
+
207
+ JavaScript-style aliases are also available: `createComic`, `getComic`,
208
+ `continueComic`, `regeneratePanel`, `updateComicPanel`, `waitForCompletion`,
209
+ `createAndWait`, and `waitForMany`.
210
+
211
+ Alias examples:
212
+
213
+ ```python
214
+ created = client.comic.createComic({
215
+ "prompt": "A 4-panel comic about a lighthouse keeper finding a map.",
216
+ "fixPanelNum": 4,
217
+ })
218
+ print("comic.createComic:", created["id"], created.get("status"))
219
+
220
+ fetched = client.comic.getComic(created["id"])
221
+ print("comic.getComic:", fetched["id"], fetched.get("status"))
222
+
223
+ done = client.comic.waitForCompletion(created["id"], timeout_ms=WAIT_TIMEOUT_MS)
224
+ print("comic.waitForCompletion:", done["id"], done.get("status"))
225
+ ```
226
+
227
+ ## Animation API
228
+
229
+ ```python
230
+ video = client.animation.create({
231
+ "prompt": "A heroic fox detective walks through neon rain, cinematic camera move.",
232
+ "videoOptions": {
233
+ "duration": 5,
234
+ "resolution": "720p",
235
+ "aspect_ratio": "16:9",
236
+ },
237
+ })
238
+
239
+ finished = client.animation.wait_for_completion(
240
+ video["id"],
241
+ interval_ms=5000,
242
+ timeout_ms=30 * 60 * 1000,
243
+ )
244
+ ```
245
+
246
+ Animation methods:
247
+
248
+ - `llamagen.animation.create(params)` calls `POST /v1/artworks/generations`.
249
+ - `llamagen.animation.get(id)` calls `GET /v1/artworks/generations/{id}`.
250
+ - `llamagen.animation.wait_for_completion(id, **options)` polls video status.
251
+ - `llamagen.animation.create_and_wait(params, **options)` creates and polls.
252
+
253
+ `llamagen.animations` is an alias for `llamagen.animation`.
254
+
255
+ Animation examples:
256
+
257
+ ```python
258
+ # Create an animation job.
259
+ video = client.animation.create({
260
+ "prompt": "A heroic fox detective walks through neon rain, cinematic camera move.",
261
+ "videoOptions": {
262
+ "duration": 5,
263
+ "resolution": "720p",
264
+ "aspect_ratio": "16:9",
265
+ },
266
+ })
267
+ print("animation.create:", video["id"], video.get("status"))
268
+
269
+ # Fetch and wait.
270
+ fetched = client.animation.get(video["id"])
271
+ print("animation.get:", fetched["id"], fetched.get("status"))
272
+
273
+ finished = client.animation.wait_for_completion(video["id"], timeout_ms=WAIT_TIMEOUT_MS)
274
+ print("animation.wait_for_completion:", finished["id"], finished.get("status"))
275
+
276
+ # Create and wait in one call.
277
+ finished = client.animation.create_and_wait({
278
+ "prompt": "A paper boat sailing across a watercolor city at sunrise.",
279
+ "videoOptions": {
280
+ "duration": 5,
281
+ "resolution": "720p",
282
+ "aspect_ratio": "16:9",
283
+ },
284
+ }, timeout_ms=WAIT_TIMEOUT_MS)
285
+ print("animation.create_and_wait:", finished["id"], finished.get("status"))
286
+
287
+ # Alias form.
288
+ same_result = client.animations.waitForCompletion(video["id"], timeout_ms=WAIT_TIMEOUT_MS)
289
+ print("animations.waitForCompletion:", same_result["id"], same_result.get("status"))
290
+ ```
291
+
292
+ ## Webhooks
293
+
294
+ ```python
295
+ import hashlib
296
+ import hmac
297
+ import json
298
+ import time
299
+
300
+ from llamagen import construct_webhook_event, verify_webhook_signature
301
+
302
+ payload = json.dumps({"type": "comic.completed", "data": {"id": "cm_demo"}}, separators=(",", ":"))
303
+ secret = "whsec_demo"
304
+ timestamp = str(int(time.time()))
305
+ signature = hmac.new(
306
+ secret.encode("utf-8"),
307
+ f"{timestamp}.{payload}".encode("utf-8"),
308
+ hashlib.sha256,
309
+ ).hexdigest()
310
+ headers = {
311
+ "X-Llama-Webhook-Timestamp": timestamp,
312
+ "X-Llama-Webhook-Signature": f"v1={signature}",
313
+ }
314
+
315
+ is_valid = verify_webhook_signature(
316
+ payload=payload,
317
+ headers=headers,
318
+ secret=secret,
319
+ )
320
+ event = construct_webhook_event(
321
+ payload=payload,
322
+ headers=headers,
323
+ secret=secret,
324
+ )
325
+ print("verify_webhook_signature:", is_valid)
326
+ print("construct_webhook_event:", event["type"], event["data"]["id"])
327
+ ```
328
+
329
+ The helper verifies `X-Llama-Webhook-Timestamp` and
330
+ `X-Llama-Webhook-Signature` with HMAC SHA-256 and a default 5-minute tolerance.
331
+
332
+ ## Errors
333
+
334
+ ```python
335
+ from llamagen import LlamaGenAPIError, LlamaGenConnectionError, LlamaGenTimeoutError
336
+
337
+ try:
338
+ usage = client.comic.usage()
339
+ except LlamaGenAPIError as error:
340
+ print(error.status, error.data)
341
+ except LlamaGenConnectionError as error:
342
+ print("Network connection failed:", error)
343
+ except LlamaGenTimeoutError as error:
344
+ print("Request timed out:", error)
345
+ ```
346
+
347
+ - `LlamaGenAPIError`: the API returned a non-2xx response, such as `401`, `403`, or `429`.
348
+ - `LlamaGenConnectionError`: the SDK could not connect to the API after retries.
349
+ - `LlamaGenTimeoutError`: the request timed out after retries.
@@ -0,0 +1,324 @@
1
+ # llamagen-python
2
+
3
+ Official Python SDK for LlamaGen's Comic API and Animation API.
4
+
5
+ Homepage: <https://llamagen.ai/comic-api>
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install llamagen-python
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ```python
16
+ import os
17
+ from pathlib import Path
18
+
19
+ from llamagen import LlamaGenClient
20
+
21
+ WAIT_TIMEOUT_MS = 30 * 60 * 1000
22
+
23
+ client = LlamaGenClient(
24
+ api_key=os.environ["LLAMAGEN_API_KEY"],
25
+ timeout_ms=30000,
26
+ max_retries=5,
27
+ retry_delay_ms=500,
28
+ )
29
+
30
+ created = client.comic.create({
31
+ "prompt": "A 4-panel comic about Leo finding a glowing key in a quiet library.",
32
+ "style": "manga",
33
+ "fixPanelNum": 4,
34
+ })
35
+ print("comic.create:", created["id"], created.get("status"))
36
+
37
+ done = client.comic.wait_for_completion(created["id"], timeout_ms=WAIT_TIMEOUT_MS)
38
+ print("comic.wait_for_completion:", done["id"], done.get("status"), done.get("comics"))
39
+ ```
40
+
41
+ The full runnable demo is in [`examples/quickstart.py`](examples/quickstart.py).
42
+ It loads `LLAMAGEN_API_KEY` from the environment or a local `.env` file.
43
+ Set `LLAMAGEN_UPLOAD_FILE=/path/to/reference.png` to run the upload demo, and
44
+ set `LLAMAGEN_RUN_OPTIONAL_DEMOS=1` to run extra paid generation examples.
45
+
46
+ ## Client
47
+
48
+ ```python
49
+ client = LlamaGenClient(
50
+ api_key="YOUR_API_KEY",
51
+ base_url="https://api.llamagen.ai/v1",
52
+ timeout_ms=30000,
53
+ max_retries=5,
54
+ retry_delay_ms=500,
55
+ )
56
+ ```
57
+
58
+ The SDK uses Python's standard library HTTP stack and does not require runtime
59
+ dependencies.
60
+
61
+ ## Comic API
62
+
63
+ Use `llamagen.comic` to create comics, manga, webtoons, storyboards,
64
+ consistent-character pages, and panel edits.
65
+
66
+ ```python
67
+ generation = client.comic.create({
68
+ "prompt": "The Little Prince meets the Fox in a luminous desert.",
69
+ "style": "storybook manga",
70
+ "size": "1024x1024",
71
+ "pagination": {"totalPages": 2, "panelsPerPage": 4},
72
+ "comicRoles": [
73
+ {
74
+ "name": "The Little Prince",
75
+ "image": "https://example.com/prince.png",
76
+ "clothing": "green coat and yellow scarf",
77
+ }
78
+ ],
79
+ "attachments": [{"type": "image", "url": "https://example.com/reference.png"}],
80
+ "language": "en",
81
+ "upscale": "2K",
82
+ })
83
+ ```
84
+
85
+ Comic methods:
86
+
87
+ - `llamagen.comic.create(params)` calls `POST /v1/comics/generations`.
88
+ - `llamagen.comic.get(id, page=None, panel=None)` calls `GET /v1/comics/generations/{id}`.
89
+ - `llamagen.comic.continue_write(id, params)` extends an existing comic.
90
+ - `llamagen.comic.update_panel(id, params)` regenerates one panel.
91
+ - `llamagen.comic.usage()` calls `GET /v1/comics/usage`.
92
+ - `llamagen.comic.upload(file, filename=None)` calls `POST /v1/comics/upload`.
93
+ - `llamagen.comic.wait_for_completion(id, **options)` polls until a terminal status.
94
+ - `llamagen.comic.create_and_wait(params, **options)` creates and polls.
95
+ - `llamagen.comic.create_batch(params_list, concurrency=3)` submits many jobs.
96
+ - `llamagen.comic.wait_for_many(ids, concurrency=3)` polls many jobs.
97
+
98
+ Comic examples:
99
+
100
+ ```python
101
+ # Create a comic generation.
102
+ created = client.comic.create({
103
+ "prompt": "A 4-panel comic about Leo finding a glowing key in a quiet library.",
104
+ "style": "manga",
105
+ "fixPanelNum": 4,
106
+ })
107
+ print("comic.create:", created["id"], created.get("status"))
108
+
109
+ # Fetch the whole generation, or one page/panel.
110
+ fetched = client.comic.get(created["id"])
111
+ print("comic.get:", fetched["id"], fetched.get("status"))
112
+
113
+ panel = client.comic.get(created["id"], page=0, panel=0)
114
+ print("comic.get panel:", panel.get("id", created["id"]), panel.get("status"))
115
+
116
+ # Wait until the generation reaches a terminal status.
117
+ # The default timeout is 30 minutes.
118
+ done = client.comic.wait_for_completion(created["id"], timeout_ms=WAIT_TIMEOUT_MS)
119
+ print("comic.wait_for_completion:", done["id"], done.get("status"), done.get("comics"))
120
+
121
+ # Create and wait in one call.
122
+ finished = client.comic.create_and_wait({
123
+ "prompt": "A cozy 3-panel comic about a robot learning to bake bread.",
124
+ "style": "storybook",
125
+ "fixPanelNum": 3,
126
+ }, timeout_ms=WAIT_TIMEOUT_MS)
127
+ print("comic.create_and_wait:", finished["id"], finished.get("status"))
128
+
129
+ # Continue an existing comic.
130
+ continued = client.comic.continue_write(
131
+ created["id"],
132
+ {
133
+ "prompt": "Continue the story with Leo opening a secret reading room.",
134
+ "pagination": {"totalPages": 1, "panelsPerPage": 4},
135
+ },
136
+ )
137
+ print("comic.continue_write:", continued["id"], continued.get("status"))
138
+
139
+ # Regenerate one panel.
140
+ updated = client.comic.update_panel(
141
+ created["id"],
142
+ {
143
+ "panelIndex": 0,
144
+ "prompt": "Make the glowing key brighter and add dramatic moonlight.",
145
+ },
146
+ )
147
+ print("comic.update_panel:", updated["id"], updated.get("status"))
148
+
149
+ # Check usage.
150
+ usage = client.comic.usage()
151
+ print("comic.usage:", usage)
152
+
153
+ # Upload a local reference image.
154
+ upload_file = os.environ.get("LLAMAGEN_UPLOAD_FILE")
155
+ if upload_file:
156
+ uploaded = client.comic.upload(Path(upload_file))
157
+ print("comic.upload:", uploaded)
158
+
159
+ # Submit many jobs and wait for many jobs.
160
+ batch = client.comic.create_batch(
161
+ [
162
+ {
163
+ "prompt": "A 2-panel comic about a cloud trying on hats.",
164
+ "style": "cartoon",
165
+ "fixPanelNum": 2,
166
+ },
167
+ {
168
+ "prompt": "A 2-panel comic about a tiny train crossing a desk.",
169
+ "style": "manga",
170
+ "fixPanelNum": 2,
171
+ },
172
+ ],
173
+ concurrency=2,
174
+ )
175
+ ids = [item["result"]["id"] for item in batch if "result" in item]
176
+ print("comic.create_batch:", ids)
177
+
178
+ finished_many = client.comic.wait_for_many(ids, concurrency=2, timeout_ms=WAIT_TIMEOUT_MS)
179
+ print("comic.wait_for_many:", [(item["id"], item.get("status")) for item in finished_many])
180
+ ```
181
+
182
+ JavaScript-style aliases are also available: `createComic`, `getComic`,
183
+ `continueComic`, `regeneratePanel`, `updateComicPanel`, `waitForCompletion`,
184
+ `createAndWait`, and `waitForMany`.
185
+
186
+ Alias examples:
187
+
188
+ ```python
189
+ created = client.comic.createComic({
190
+ "prompt": "A 4-panel comic about a lighthouse keeper finding a map.",
191
+ "fixPanelNum": 4,
192
+ })
193
+ print("comic.createComic:", created["id"], created.get("status"))
194
+
195
+ fetched = client.comic.getComic(created["id"])
196
+ print("comic.getComic:", fetched["id"], fetched.get("status"))
197
+
198
+ done = client.comic.waitForCompletion(created["id"], timeout_ms=WAIT_TIMEOUT_MS)
199
+ print("comic.waitForCompletion:", done["id"], done.get("status"))
200
+ ```
201
+
202
+ ## Animation API
203
+
204
+ ```python
205
+ video = client.animation.create({
206
+ "prompt": "A heroic fox detective walks through neon rain, cinematic camera move.",
207
+ "videoOptions": {
208
+ "duration": 5,
209
+ "resolution": "720p",
210
+ "aspect_ratio": "16:9",
211
+ },
212
+ })
213
+
214
+ finished = client.animation.wait_for_completion(
215
+ video["id"],
216
+ interval_ms=5000,
217
+ timeout_ms=30 * 60 * 1000,
218
+ )
219
+ ```
220
+
221
+ Animation methods:
222
+
223
+ - `llamagen.animation.create(params)` calls `POST /v1/artworks/generations`.
224
+ - `llamagen.animation.get(id)` calls `GET /v1/artworks/generations/{id}`.
225
+ - `llamagen.animation.wait_for_completion(id, **options)` polls video status.
226
+ - `llamagen.animation.create_and_wait(params, **options)` creates and polls.
227
+
228
+ `llamagen.animations` is an alias for `llamagen.animation`.
229
+
230
+ Animation examples:
231
+
232
+ ```python
233
+ # Create an animation job.
234
+ video = client.animation.create({
235
+ "prompt": "A heroic fox detective walks through neon rain, cinematic camera move.",
236
+ "videoOptions": {
237
+ "duration": 5,
238
+ "resolution": "720p",
239
+ "aspect_ratio": "16:9",
240
+ },
241
+ })
242
+ print("animation.create:", video["id"], video.get("status"))
243
+
244
+ # Fetch and wait.
245
+ fetched = client.animation.get(video["id"])
246
+ print("animation.get:", fetched["id"], fetched.get("status"))
247
+
248
+ finished = client.animation.wait_for_completion(video["id"], timeout_ms=WAIT_TIMEOUT_MS)
249
+ print("animation.wait_for_completion:", finished["id"], finished.get("status"))
250
+
251
+ # Create and wait in one call.
252
+ finished = client.animation.create_and_wait({
253
+ "prompt": "A paper boat sailing across a watercolor city at sunrise.",
254
+ "videoOptions": {
255
+ "duration": 5,
256
+ "resolution": "720p",
257
+ "aspect_ratio": "16:9",
258
+ },
259
+ }, timeout_ms=WAIT_TIMEOUT_MS)
260
+ print("animation.create_and_wait:", finished["id"], finished.get("status"))
261
+
262
+ # Alias form.
263
+ same_result = client.animations.waitForCompletion(video["id"], timeout_ms=WAIT_TIMEOUT_MS)
264
+ print("animations.waitForCompletion:", same_result["id"], same_result.get("status"))
265
+ ```
266
+
267
+ ## Webhooks
268
+
269
+ ```python
270
+ import hashlib
271
+ import hmac
272
+ import json
273
+ import time
274
+
275
+ from llamagen import construct_webhook_event, verify_webhook_signature
276
+
277
+ payload = json.dumps({"type": "comic.completed", "data": {"id": "cm_demo"}}, separators=(",", ":"))
278
+ secret = "whsec_demo"
279
+ timestamp = str(int(time.time()))
280
+ signature = hmac.new(
281
+ secret.encode("utf-8"),
282
+ f"{timestamp}.{payload}".encode("utf-8"),
283
+ hashlib.sha256,
284
+ ).hexdigest()
285
+ headers = {
286
+ "X-Llama-Webhook-Timestamp": timestamp,
287
+ "X-Llama-Webhook-Signature": f"v1={signature}",
288
+ }
289
+
290
+ is_valid = verify_webhook_signature(
291
+ payload=payload,
292
+ headers=headers,
293
+ secret=secret,
294
+ )
295
+ event = construct_webhook_event(
296
+ payload=payload,
297
+ headers=headers,
298
+ secret=secret,
299
+ )
300
+ print("verify_webhook_signature:", is_valid)
301
+ print("construct_webhook_event:", event["type"], event["data"]["id"])
302
+ ```
303
+
304
+ The helper verifies `X-Llama-Webhook-Timestamp` and
305
+ `X-Llama-Webhook-Signature` with HMAC SHA-256 and a default 5-minute tolerance.
306
+
307
+ ## Errors
308
+
309
+ ```python
310
+ from llamagen import LlamaGenAPIError, LlamaGenConnectionError, LlamaGenTimeoutError
311
+
312
+ try:
313
+ usage = client.comic.usage()
314
+ except LlamaGenAPIError as error:
315
+ print(error.status, error.data)
316
+ except LlamaGenConnectionError as error:
317
+ print("Network connection failed:", error)
318
+ except LlamaGenTimeoutError as error:
319
+ print("Request timed out:", error)
320
+ ```
321
+
322
+ - `LlamaGenAPIError`: the API returned a non-2xx response, such as `401`, `403`, or `429`.
323
+ - `LlamaGenConnectionError`: the SDK could not connect to the API after retries.
324
+ - `LlamaGenTimeoutError`: the request timed out after retries.