saymaker 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.
saymaker/__init__.py ADDED
@@ -0,0 +1,271 @@
1
+ """Generate images and video on SayMaker (https://saymaker.ai) from Python.
2
+
3
+ from saymaker import generate, generate_video
4
+ url = generate("a paper-cut layered mountain range at dusk")
5
+ clip = generate_video("a paper boat drifting down a rain gutter, low angle")
6
+
7
+ One API key runs the whole SayMaker shelf: Nano Banana 2, GPT Image 2.5,
8
+ Seedream 5.0, Veo 3.1, Kling 3.0, Seedance 2.0, MiniMax H3 and more, on your
9
+ own credits. Create a key at https://saymaker.ai/settings/apikeys and pass it
10
+ as ``api_key=`` or set ``SAYMAKER_API_KEY``.
11
+
12
+ Standard library only.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import os
19
+ import time
20
+ import urllib.error
21
+ import urllib.request
22
+ from typing import Callable, Optional
23
+
24
+ __version__ = "0.1.0"
25
+ __all__ = [
26
+ "generate",
27
+ "generate_video",
28
+ "SayMakerError",
29
+ "AuthError",
30
+ "QuotaError",
31
+ "PlanError",
32
+ "RejectedError",
33
+ "MODELS",
34
+ "BASE",
35
+ ]
36
+
37
+ BASE = "https://saymaker.ai"
38
+ KEYS_URL = BASE + "/settings/apikeys"
39
+
40
+ #: Model id -> the routing field the API expects next to it. Ids not listed
41
+ #: here are sent with "kie"; the full list with inputs is on saymaker.ai/models.
42
+ MODELS = {
43
+ # image
44
+ "saymaker-image-v1": "kie",
45
+ "nano-banana-2-lite": "kie",
46
+ "nano-banana-2": "poyo",
47
+ "nano-banana-pro": "poyo",
48
+ "gpt-image-2": "poyo",
49
+ "gpt-image-2-5-flare": "kie",
50
+ "seedream-5-lite": "poyo",
51
+ "seedream-5-pro": "kie",
52
+ "qwen-image-3-pro": "kie",
53
+ "grok-imagine-image-2-0": "poyo",
54
+ # video
55
+ "minimax-h3-fast": "vgenv",
56
+ "minimax-h3": "poyo",
57
+ "seedance-2": "kie",
58
+ "seedance-2-fast": "kie",
59
+ "seedance-2-5": "kie",
60
+ "veo-3-1": "kie",
61
+ "kling-3-0": "kie",
62
+ "kling-3-0-turbo": "kie",
63
+ "wan-3-0": "kie",
64
+ "ltx-2-5-fast": "replicate",
65
+ }
66
+
67
+ #: The cheapest text-to-image model; the edit default takes an input image.
68
+ DEFAULT_IMAGE_MODEL = "saymaker-image-v1"
69
+ DEFAULT_EDIT_MODEL = "nano-banana-2-lite"
70
+ #: The video model a free account can run (480p or 768p, 4 to 15 seconds).
71
+ DEFAULT_VIDEO_MODEL = "minimax-h3-fast"
72
+
73
+
74
+ class SayMakerError(RuntimeError):
75
+ """Base class. Catch a subclass to decide what to do next."""
76
+
77
+
78
+ class AuthError(SayMakerError):
79
+ """No API key, or the key is invalid or deleted."""
80
+
81
+
82
+ class QuotaError(SayMakerError):
83
+ """The account is out of credits."""
84
+
85
+
86
+ class PlanError(SayMakerError):
87
+ """This model needs a paid plan or a credit pack."""
88
+
89
+
90
+ class RejectedError(SayMakerError):
91
+ """The content filter or the model refused the prompt. Reword it."""
92
+
93
+
94
+ Transport = Callable[[str, str, Optional[dict], dict], dict]
95
+
96
+
97
+ def _http(method: str, url: str, body: Optional[dict], headers: dict) -> dict:
98
+ headers = {**headers, "User-Agent": f"saymaker-python/{__version__}"}
99
+ data = None
100
+ if body is not None:
101
+ data = json.dumps(body).encode()
102
+ headers["Content-Type"] = "application/json"
103
+ req = urllib.request.Request(url, data=data, headers=headers, method=method)
104
+ try:
105
+ with urllib.request.urlopen(req, timeout=60) as r:
106
+ return json.loads(r.read())
107
+ except urllib.error.HTTPError as e:
108
+ return json.loads(e.read() or b"{}")
109
+
110
+
111
+ def _parse_submit(env: dict) -> str:
112
+ # Refusals arrive as HTTP 200 with code -1; the status code alone reads as success.
113
+ if env.get("code") != 0:
114
+ msg = str(env.get("message") or "request refused")
115
+ low = msg.lower()
116
+ if "api key" in low or "no auth" in low:
117
+ raise AuthError(f"{msg} — create a key at {KEYS_URL}")
118
+ if "insufficient credits" in low:
119
+ raise QuotaError(msg)
120
+ if "subscription" in low or "plan" in low or "sign in" in low:
121
+ raise PlanError(msg)
122
+ raise SayMakerError(msg)
123
+ data = env.get("data") or {}
124
+ # A signed-out request is also a 200 with code 0 — only `wall` tells it apart.
125
+ if data.get("wall"):
126
+ raise AuthError(f"SayMaker needs an API key; create one at {KEYS_URL}")
127
+ task = data.get("id")
128
+ if not task:
129
+ raise SayMakerError("the service returned no task id")
130
+ return str(task)
131
+
132
+
133
+ def _run(
134
+ media: str,
135
+ scene: str,
136
+ model: str,
137
+ prompt: str,
138
+ options: dict,
139
+ api_key: Optional[str],
140
+ timeout: float,
141
+ poll_every: float,
142
+ base_url: str,
143
+ transport: Optional[Transport],
144
+ ) -> str:
145
+ if not str(prompt or "").strip():
146
+ raise ValueError("prompt is required")
147
+ key = api_key or os.environ.get("SAYMAKER_API_KEY") or None
148
+ if not key:
149
+ raise AuthError(f"pass api_key= or set SAYMAKER_API_KEY; create a key at {KEYS_URL}")
150
+ http = transport or _http
151
+ headers = {"Authorization": f"Bearer {key}"}
152
+ task = _parse_submit(
153
+ http(
154
+ "POST",
155
+ base_url + "/api/ai/generate",
156
+ {
157
+ "provider": MODELS.get(model, "kie"),
158
+ "mediaType": media,
159
+ "model": model,
160
+ "scene": scene,
161
+ "prompt": prompt,
162
+ "options": options,
163
+ },
164
+ headers,
165
+ )
166
+ )
167
+
168
+ # Free-account runs wait in a queue and only reach the model on the poll
169
+ # that crosses the end of the wait, so polling is what starts the work.
170
+ deadline = time.monotonic() + timeout
171
+ while time.monotonic() < deadline:
172
+ time.sleep(poll_every)
173
+ try:
174
+ env = http("POST", base_url + "/api/ai/query", {"taskId": task}, headers)
175
+ except (OSError, ValueError):
176
+ continue # a dropped poll is not a failed generation
177
+ if env.get("code") != 0:
178
+ continue
179
+ data = env.get("data") or {}
180
+ for field in ("cleanImages", "images", "videos"):
181
+ if data.get(field):
182
+ return data[field][0]
183
+ if str(data.get("status") or "").lower() in {"failed", "error"}:
184
+ raise RejectedError("the run failed; reword the prompt rather than retrying the same text")
185
+ raise SayMakerError(f"deadline passed after {timeout:g}s; the run may still finish in your SayMaker history")
186
+
187
+
188
+ def _check_url(image_url: Optional[str]) -> None:
189
+ if image_url is not None and not image_url.startswith(("http://", "https://")):
190
+ raise ValueError("image_url must be a public http(s) url")
191
+
192
+
193
+ def generate(
194
+ prompt: str,
195
+ *,
196
+ image_url: Optional[str] = None,
197
+ model: Optional[str] = None,
198
+ aspect_ratio: Optional[str] = None,
199
+ api_key: Optional[str] = None,
200
+ timeout: float = 360.0,
201
+ poll_every: float = 5.0,
202
+ base_url: str = BASE,
203
+ transport: Optional[Transport] = None,
204
+ ) -> str:
205
+ """Generate one image — or edit ``image_url`` — and return its URL.
206
+
207
+ For an edit the prompt describes the change and should name what must stay
208
+ ("keep the same face and lighting"). Raises a :class:`SayMakerError`
209
+ subclass on a refusal, an empty balance or a timeout.
210
+ """
211
+ _check_url(image_url)
212
+ chosen = model or (DEFAULT_EDIT_MODEL if image_url else DEFAULT_IMAGE_MODEL)
213
+ # `size` carries the aspect ratio — the site's own field name. An edit
214
+ # defaults to "auto", which keeps the source framing.
215
+ options: dict = {"size": aspect_ratio or ("auto" if image_url else "1:1"), "resolution": "1K", "n": 1}
216
+ if image_url:
217
+ options["image_input"] = [image_url]
218
+ return _run(
219
+ "image",
220
+ "image-to-image" if image_url else "text-to-image",
221
+ chosen,
222
+ prompt,
223
+ options,
224
+ api_key,
225
+ timeout,
226
+ poll_every,
227
+ base_url,
228
+ transport,
229
+ )
230
+
231
+
232
+ def generate_video(
233
+ prompt: str,
234
+ *,
235
+ image_url: Optional[str] = None,
236
+ model: str = DEFAULT_VIDEO_MODEL,
237
+ duration: Optional[int] = None,
238
+ resolution: Optional[str] = None,
239
+ aspect_ratio: str = "16:9",
240
+ sound: bool = True,
241
+ api_key: Optional[str] = None,
242
+ timeout: float = 900.0,
243
+ poll_every: float = 10.0,
244
+ base_url: str = BASE,
245
+ transport: Optional[Transport] = None,
246
+ ) -> str:
247
+ """Generate one clip — or animate ``image_url`` as the first frame — and return its URL.
248
+
249
+ Video takes minutes. ``model`` defaults to MiniMax H3 Fast, the one a free
250
+ account can run; Veo 3.1, Kling 3.0 and Seedance 2.0 need a plan or a pack.
251
+ """
252
+ _check_url(image_url)
253
+ options: dict = {"aspect_ratio": aspect_ratio, "sound": sound}
254
+ if image_url:
255
+ options["image_input"] = [image_url]
256
+ if duration:
257
+ options["duration"] = duration
258
+ if resolution:
259
+ options["resolution"] = resolution
260
+ return _run(
261
+ "video",
262
+ "image-to-video" if image_url else "text-to-video",
263
+ model,
264
+ prompt,
265
+ options,
266
+ api_key,
267
+ timeout,
268
+ poll_every,
269
+ base_url,
270
+ transport,
271
+ )
@@ -0,0 +1,91 @@
1
+ Metadata-Version: 2.4
2
+ Name: saymaker
3
+ Version: 0.1.0
4
+ Summary: Generate images and video from Python on SayMaker: Veo 3.1, Kling 3.0, Seedance 2.0, Nano Banana 2, GPT Image 2.5 and more with one API key
5
+ Author: SayMaker
6
+ License: MIT
7
+ Project-URL: Homepage, https://saymaker.ai/?utm_source=pypi&utm_medium=package
8
+ Project-URL: Video models, https://saymaker.ai/video?utm_source=pypi&utm_medium=package
9
+ Project-URL: API keys, https://saymaker.ai/settings/apikeys?utm_source=pypi&utm_medium=package
10
+ Project-URL: Source, https://github.com/hanshs474/saymaker-py
11
+ Keywords: ai,image-generation,video-generation,text-to-video,text-to-image,veo,kling,seedance
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Multimedia :: Graphics
17
+ Classifier: Topic :: Multimedia :: Video
18
+ Requires-Python: >=3.8
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Dynamic: license-file
22
+
23
+ # saymaker
24
+
25
+ Generate images and video from Python on [SayMaker](https://saymaker.ai/?utm_source=pypi&utm_medium=package): one API key runs Veo 3.1, Kling 3.0, Seedance 2.0, MiniMax H3, Nano Banana 2, GPT Image 2.5, Seedream 5.0 and the rest of the shelf on your own credits.
26
+
27
+ ```python
28
+ from saymaker import generate, generate_video
29
+
30
+ url = generate("a paper-cut layered mountain range at dusk, warm rim light")
31
+ clip = generate_video("a paper boat drifting down a rain gutter, low angle, slow push-in")
32
+ ```
33
+
34
+ Standard library only, no dependencies. Each call returns the URL of the finished file.
35
+
36
+ ## API key
37
+
38
+ Sign up on [saymaker.ai](https://saymaker.ai/?utm_source=pypi&utm_medium=package) (new accounts get sign-up credits), create a key at [saymaker.ai/settings/apikeys](https://saymaker.ai/settings/apikeys?utm_source=pypi&utm_medium=package), then either pass it or export it:
39
+
40
+ ```bash
41
+ export SAYMAKER_API_KEY=sk-...
42
+ ```
43
+
44
+ A key runs on your account exactly like the site does: same models, same credit prices, same plan, and every run lands in your library at [saymaker.ai/history](https://saymaker.ai/history?utm_source=pypi&utm_medium=package).
45
+
46
+ ## Images
47
+
48
+ ```python
49
+ generate("isometric diorama of a ramen shop at night", aspect_ratio="16:9")
50
+ generate("product shot of a glass perfume bottle on wet slate", model="gpt-image-2-5-flare")
51
+ ```
52
+
53
+ The default model is `saymaker-image-v1`, the cheapest text-to-image run. Pass `model=` for `nano-banana-2`, `gpt-image-2-5-flare`, `seedream-5-pro`, `qwen-image-3-pro` and the others listed in `saymaker.MODELS`.
54
+
55
+ ### Editing a photo
56
+
57
+ Pass `image_url` and the prompt describes the change rather than the picture:
58
+
59
+ ```python
60
+ generate(
61
+ "change the jacket to dark green, keep the face, pose and background exactly as they are",
62
+ image_url="https://example.com/portrait.jpg",
63
+ )
64
+ ```
65
+
66
+ Naming what must stay is what decides whether the edit holds. Prompts that only name the change tend to drift the whole picture.
67
+
68
+ ## Video
69
+
70
+ ```python
71
+ generate_video("a fox trotting through fresh snow at dawn, tracking shot", duration=6)
72
+ generate_video("the camera slowly orbits the statue", image_url="https://example.com/statue.jpg")
73
+ generate_video("a street drummer in the rain", model="veo-3-1", resolution="1080p")
74
+ ```
75
+
76
+ Video takes minutes; `generate_video` waits up to 15 minutes by default (`timeout=`). The default model is `minimax-h3-fast`, the one a free account can run (480p or 768p, 4 to 15 seconds). Veo 3.1, Kling 3.0, Seedance 2.0 and the other video models need a plan or a credit pack; see [pricing](https://saymaker.ai/pricing?utm_source=pypi&utm_medium=package). Each model's options are on its page, e.g. [Veo 3.1](https://saymaker.ai/video/veo-3-1?utm_source=pypi&utm_medium=package), [Kling 3.0](https://saymaker.ai/video/kling-3-0?utm_source=pypi&utm_medium=package), [Seedance 2.0](https://saymaker.ai/video/seedance-2?utm_source=pypi&utm_medium=package).
77
+
78
+ ## Errors worth catching
79
+
80
+ Each failure is its own subclass of `SayMakerError`, so you can branch on what to do next:
81
+
82
+ - `AuthError` — no key, or the key is wrong or deleted
83
+ - `QuotaError` — the account is out of credits
84
+ - `PlanError` — that model needs a paid plan or a credit pack
85
+ - `RejectedError` — the content filter or the model refused the prompt; rewrite it rather than retrying
86
+
87
+ The API answers refusals with HTTP 200 and an error code in the body, so a client that only checks the status code reports a refusal as success. This one reads the body.
88
+
89
+ Free-account output carries a watermark; a paid plan returns the clean file.
90
+
91
+ MIT licensed. Made by [SayMaker](https://saymaker.ai/?utm_source=pypi&utm_medium=package), the AI video generator agent.
@@ -0,0 +1,6 @@
1
+ saymaker/__init__.py,sha256=B9eTxyMIPUjTUZQzsL7GoisrLxevQ8QesfgMSklg624,8680
2
+ saymaker-0.1.0.dist-info/licenses/LICENSE,sha256=qDYZDkMKi8miQ7F9rMaDn5siXO3-f0mNsH3Ud33atMo,1087
3
+ saymaker-0.1.0.dist-info/METADATA,sha256=e5tKJj6bSEB1UeOcRZXnXxXak7BLqSpjK-lQKH2U4Yk,4707
4
+ saymaker-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
5
+ saymaker-0.1.0.dist-info/top_level.txt,sha256=8S3WlAf1eE9jHMWXlyojD2iYkd3sQ-AS6eaZTs1e734,9
6
+ saymaker-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SayMaker (https://saymaker.ai)
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.
@@ -0,0 +1 @@
1
+ saymaker