saymaker 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.
- saymaker-0.1.0/LICENSE +21 -0
- saymaker-0.1.0/PKG-INFO +91 -0
- saymaker-0.1.0/README.md +69 -0
- saymaker-0.1.0/pyproject.toml +30 -0
- saymaker-0.1.0/setup.cfg +4 -0
- saymaker-0.1.0/src/saymaker/__init__.py +271 -0
- saymaker-0.1.0/src/saymaker.egg-info/PKG-INFO +91 -0
- saymaker-0.1.0/src/saymaker.egg-info/SOURCES.txt +9 -0
- saymaker-0.1.0/src/saymaker.egg-info/dependency_links.txt +1 -0
- saymaker-0.1.0/src/saymaker.egg-info/top_level.txt +1 -0
- saymaker-0.1.0/tests/test_saymaker.py +76 -0
saymaker-0.1.0/LICENSE
ADDED
|
@@ -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.
|
saymaker-0.1.0/PKG-INFO
ADDED
|
@@ -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.
|
saymaker-0.1.0/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# saymaker
|
|
2
|
+
|
|
3
|
+
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.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
from saymaker import generate, generate_video
|
|
7
|
+
|
|
8
|
+
url = generate("a paper-cut layered mountain range at dusk, warm rim light")
|
|
9
|
+
clip = generate_video("a paper boat drifting down a rain gutter, low angle, slow push-in")
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Standard library only, no dependencies. Each call returns the URL of the finished file.
|
|
13
|
+
|
|
14
|
+
## API key
|
|
15
|
+
|
|
16
|
+
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:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
export SAYMAKER_API_KEY=sk-...
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
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).
|
|
23
|
+
|
|
24
|
+
## Images
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
generate("isometric diorama of a ramen shop at night", aspect_ratio="16:9")
|
|
28
|
+
generate("product shot of a glass perfume bottle on wet slate", model="gpt-image-2-5-flare")
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
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`.
|
|
32
|
+
|
|
33
|
+
### Editing a photo
|
|
34
|
+
|
|
35
|
+
Pass `image_url` and the prompt describes the change rather than the picture:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
generate(
|
|
39
|
+
"change the jacket to dark green, keep the face, pose and background exactly as they are",
|
|
40
|
+
image_url="https://example.com/portrait.jpg",
|
|
41
|
+
)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Naming what must stay is what decides whether the edit holds. Prompts that only name the change tend to drift the whole picture.
|
|
45
|
+
|
|
46
|
+
## Video
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
generate_video("a fox trotting through fresh snow at dawn, tracking shot", duration=6)
|
|
50
|
+
generate_video("the camera slowly orbits the statue", image_url="https://example.com/statue.jpg")
|
|
51
|
+
generate_video("a street drummer in the rain", model="veo-3-1", resolution="1080p")
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
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).
|
|
55
|
+
|
|
56
|
+
## Errors worth catching
|
|
57
|
+
|
|
58
|
+
Each failure is its own subclass of `SayMakerError`, so you can branch on what to do next:
|
|
59
|
+
|
|
60
|
+
- `AuthError` — no key, or the key is wrong or deleted
|
|
61
|
+
- `QuotaError` — the account is out of credits
|
|
62
|
+
- `PlanError` — that model needs a paid plan or a credit pack
|
|
63
|
+
- `RejectedError` — the content filter or the model refused the prompt; rewrite it rather than retrying
|
|
64
|
+
|
|
65
|
+
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.
|
|
66
|
+
|
|
67
|
+
Free-account output carries a watermark; a paid plan returns the clean file.
|
|
68
|
+
|
|
69
|
+
MIT licensed. Made by [SayMaker](https://saymaker.ai/?utm_source=pypi&utm_medium=package), the AI video generator agent.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "saymaker"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "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"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "SayMaker" }]
|
|
13
|
+
keywords = ["ai", "image-generation", "video-generation", "text-to-video", "text-to-image", "veo", "kling", "seedance"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: Multimedia :: Graphics",
|
|
20
|
+
"Topic :: Multimedia :: Video",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://saymaker.ai/?utm_source=pypi&utm_medium=package"
|
|
25
|
+
"Video models" = "https://saymaker.ai/video?utm_source=pypi&utm_medium=package"
|
|
26
|
+
"API keys" = "https://saymaker.ai/settings/apikeys?utm_source=pypi&utm_medium=package"
|
|
27
|
+
Source = "https://github.com/hanshs474/saymaker-py"
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.packages.find]
|
|
30
|
+
where = ["src"]
|
saymaker-0.1.0/setup.cfg
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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
saymaker
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import unittest
|
|
3
|
+
|
|
4
|
+
import saymaker
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def fake(calls, final):
|
|
8
|
+
def t(method, url, body, headers):
|
|
9
|
+
calls.append((method, url, body, headers))
|
|
10
|
+
if url.endswith("/api/ai/generate"):
|
|
11
|
+
return {"code": 0, "data": {"id": "row1"}}
|
|
12
|
+
return {"code": 0, "data": final}
|
|
13
|
+
|
|
14
|
+
return t
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SayMakerTests(unittest.TestCase):
|
|
18
|
+
def setUp(self):
|
|
19
|
+
os.environ.pop("SAYMAKER_API_KEY", None)
|
|
20
|
+
|
|
21
|
+
def test_validates(self):
|
|
22
|
+
with self.assertRaises(ValueError):
|
|
23
|
+
saymaker.generate(" ", api_key="sk-x")
|
|
24
|
+
with self.assertRaises(ValueError):
|
|
25
|
+
saymaker.generate("x", image_url="file:///x.png", api_key="sk-x")
|
|
26
|
+
|
|
27
|
+
def test_needs_a_key(self):
|
|
28
|
+
with self.assertRaises(saymaker.AuthError):
|
|
29
|
+
saymaker.generate("x")
|
|
30
|
+
|
|
31
|
+
def test_wall_is_a_200(self):
|
|
32
|
+
with self.assertRaises(saymaker.AuthError):
|
|
33
|
+
saymaker._parse_submit({"code": 0, "data": {"wall": True, "reason": "sign_in_required"}})
|
|
34
|
+
|
|
35
|
+
def test_refusals(self):
|
|
36
|
+
with self.assertRaises(saymaker.AuthError):
|
|
37
|
+
saymaker._parse_submit({"code": -1, "message": "invalid API key"})
|
|
38
|
+
with self.assertRaises(saymaker.QuotaError):
|
|
39
|
+
saymaker._parse_submit({"code": -1, "message": "insufficient credits"})
|
|
40
|
+
|
|
41
|
+
def test_image_prefers_clean_and_polls_query(self):
|
|
42
|
+
calls = []
|
|
43
|
+
final = {"status": "success", "images": ["https://cdn/m.webp"], "cleanImages": ["https://cdn/c.webp"]}
|
|
44
|
+
url = saymaker.generate("x", api_key="sk-abc", poll_every=0, base_url="https://t", transport=fake(calls, final))
|
|
45
|
+
self.assertEqual(url, "https://cdn/c.webp")
|
|
46
|
+
self.assertEqual(calls[0][3], {"Authorization": "Bearer sk-abc"})
|
|
47
|
+
self.assertEqual(calls[0][2]["model"], "saymaker-image-v1")
|
|
48
|
+
self.assertEqual(calls[0][2]["provider"], "kie")
|
|
49
|
+
self.assertEqual(calls[1][:3], ("POST", "https://t/api/ai/query", {"taskId": "row1"}))
|
|
50
|
+
|
|
51
|
+
def test_edit_sends_image_input(self):
|
|
52
|
+
calls = []
|
|
53
|
+
final = {"status": "success", "images": ["https://cdn/x.webp"]}
|
|
54
|
+
saymaker.generate("short hair", image_url="https://x/p.jpg", api_key="sk", poll_every=0, base_url="https://t", transport=fake(calls, final))
|
|
55
|
+
body = calls[0][2]
|
|
56
|
+
self.assertEqual(body["scene"], "image-to-image")
|
|
57
|
+
self.assertEqual(body["model"], "nano-banana-2-lite")
|
|
58
|
+
self.assertEqual(body["options"]["image_input"], ["https://x/p.jpg"])
|
|
59
|
+
self.assertEqual(body["options"]["size"], "auto")
|
|
60
|
+
|
|
61
|
+
def test_video_routes_by_model(self):
|
|
62
|
+
calls = []
|
|
63
|
+
final = {"status": "success", "videos": ["https://cdn/v.mp4"]}
|
|
64
|
+
url = saymaker.generate_video("a boat", api_key="sk", duration=6, poll_every=0, base_url="https://t", transport=fake(calls, final))
|
|
65
|
+
self.assertEqual(url, "https://cdn/v.mp4")
|
|
66
|
+
body = calls[0][2]
|
|
67
|
+
self.assertEqual((body["mediaType"], body["scene"], body["model"], body["provider"]), ("video", "text-to-video", "minimax-h3-fast", "vgenv"))
|
|
68
|
+
self.assertEqual(body["options"]["duration"], 6)
|
|
69
|
+
|
|
70
|
+
def test_failed_run(self):
|
|
71
|
+
with self.assertRaises(saymaker.RejectedError):
|
|
72
|
+
saymaker.generate("x", api_key="sk", poll_every=0, transport=fake([], {"status": "failed"}))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
if __name__ == "__main__":
|
|
76
|
+
unittest.main()
|