saymaker 0.1.0
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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +78 -0
- data/lib/saymaker/version.rb +3 -0
- data/lib/saymaker.rb +137 -0
- metadata +54 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 1df4020cb61123a658e27f233b48a30194df8449e12312d64b074e7d315689eb
|
|
4
|
+
data.tar.gz: 649cf523e9c79b8714ecfb302afc1857e980367b4f9c4031560acfee46d41dcd
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: '097d1f55cad0f58a78b23754e8dfd3a05842cc02c0dd1a9c1fe276d4c9532ac016f0d143e7018d94763b72c2fe7909176a58237e70f4b45f13daec50fc7812f9'
|
|
7
|
+
data.tar.gz: b52db8346f12638f3d56647b817e282a1f465f2b2eeaa43167718a94d21330236aae353d81823ca04297b86c8b2c52d1c0ca2e4abe8ee8775a352b6fea41dedb
|
data/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.
|
data/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# saymaker
|
|
2
|
+
|
|
3
|
+
Generate images and video on [SayMaker](https://saymaker.ai/?utm_source=rubygems&utm_medium=package) from your own code: 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
|
+
```ruby
|
|
6
|
+
require "saymaker"
|
|
7
|
+
|
|
8
|
+
url = SayMaker.generate("a paper-cut layered mountain range at dusk")
|
|
9
|
+
clip = SayMaker.generate_video("a paper boat drifting down a rain gutter, low angle")
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Each call waits for the run and returns the URL of the finished file. Runs on a free account wait in a queue before they start, and the wait grows with each run of the day; a paid plan starts at once. The default timeouts cover the longest wait.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
gem install saymaker
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Standard library only (`net/http` and `json`). Set `SAYMAKER_API_KEY` or pass `api_key:`.
|
|
21
|
+
|
|
22
|
+
## API key
|
|
23
|
+
|
|
24
|
+
Sign up on [saymaker.ai](https://saymaker.ai/?utm_source=rubygems&utm_medium=package) (new accounts get sign-up credits), create a key at [saymaker.ai/settings/apikeys](https://saymaker.ai/settings/apikeys?utm_source=rubygems&utm_medium=package), then pass it or export it:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
export SAYMAKER_API_KEY=sk-...
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
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=rubygems&utm_medium=package).
|
|
31
|
+
|
|
32
|
+
## Images
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
SayMaker.generate("isometric diorama of a ramen shop at night", aspect_ratio: "16:9")
|
|
36
|
+
SayMaker.generate("product shot of a glass perfume bottle on wet slate", model: "gpt-image-2-5-flare")
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The default model is `saymaker-image-v1`, the cheapest text-to-image run. Pass a model id for `nano-banana-2`, `gpt-image-2-5-flare`, `seedream-5-pro`, `qwen-image-3-pro` and the rest; see the [image models](https://saymaker.ai/image?utm_source=rubygems&utm_medium=package).
|
|
40
|
+
|
|
41
|
+
### Editing a photo
|
|
42
|
+
|
|
43
|
+
Pass an image URL and the prompt describes the change rather than the picture:
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
SayMaker.generate(
|
|
47
|
+
"change the jacket to dark green, keep the face, pose and background exactly as they are",
|
|
48
|
+
image_url: "https://example.com/portrait.jpg"
|
|
49
|
+
)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Naming what must stay is what decides whether the edit holds. Prompts that only name the change tend to drift the whole picture.
|
|
53
|
+
|
|
54
|
+
## Video
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
SayMaker.generate_video("a fox trotting through fresh snow at dawn, tracking shot", duration: 6)
|
|
58
|
+
SayMaker.generate_video("the camera slowly orbits the statue", image_url: "https://example.com/statue.jpg")
|
|
59
|
+
SayMaker.generate_video("a street drummer in the rain", model: "veo-3-1", resolution: "1080p")
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
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=rubygems&utm_medium=package). Each model's options are on its page: [Veo 3.1](https://saymaker.ai/video/veo-3-1?utm_source=rubygems&utm_medium=package), [Kling 3.0](https://saymaker.ai/video/kling-3-0?utm_source=rubygems&utm_medium=package), [Seedance 2.0](https://saymaker.ai/video/seedance-2?utm_source=rubygems&utm_medium=package).
|
|
63
|
+
|
|
64
|
+
## Errors worth catching
|
|
65
|
+
|
|
66
|
+
- `SayMaker::AuthError` — no key, or the key is wrong or deleted
|
|
67
|
+
- `SayMaker::QuotaError` — the account is out of credits
|
|
68
|
+
- `SayMaker::PlanError` — that model needs a paid plan or a credit pack
|
|
69
|
+
- `SayMaker::BusyError` — another run on this account is still rendering; free accounts run one at a time
|
|
70
|
+
- `SayMaker::RejectedError` — the content filter or the model refused the prompt; rewrite it rather than retrying
|
|
71
|
+
|
|
72
|
+
All inherit from `SayMaker::Error`.
|
|
73
|
+
|
|
74
|
+
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.
|
|
75
|
+
|
|
76
|
+
Free-account output carries a watermark; a paid plan returns the clean file.
|
|
77
|
+
|
|
78
|
+
MIT licensed. Made by [SayMaker](https://saymaker.ai/?utm_source=rubygems&utm_medium=package), the AI video generator agent.
|
data/lib/saymaker.rb
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Generate images and video from Ruby on SayMaker (https://saymaker.ai).
|
|
2
|
+
#
|
|
3
|
+
# require "saymaker"
|
|
4
|
+
# url = SayMaker.generate("a paper-cut layered mountain range at dusk")
|
|
5
|
+
# clip = SayMaker.generate_video("a fox trotting through snow", duration: 6)
|
|
6
|
+
#
|
|
7
|
+
# One API key runs the whole shelf on your own credits: Veo 3.1, Kling 3.0,
|
|
8
|
+
# Seedance 2.0, MiniMax H3, Nano Banana 2, GPT Image 2.5, Seedream 5.0 and more.
|
|
9
|
+
# Create a key at https://saymaker.ai/settings/apikeys and set SAYMAKER_API_KEY
|
|
10
|
+
# or pass api_key:. Standard library only.
|
|
11
|
+
require "json"
|
|
12
|
+
require "net/http"
|
|
13
|
+
require "uri"
|
|
14
|
+
require_relative "saymaker/version"
|
|
15
|
+
|
|
16
|
+
module SayMaker
|
|
17
|
+
BASE = "https://saymaker.ai"
|
|
18
|
+
KEYS_URL = "#{BASE}/settings/apikeys"
|
|
19
|
+
MODEL_IMAGE = "saymaker-image-v1"
|
|
20
|
+
MODEL_EDIT = "nano-banana-2-lite"
|
|
21
|
+
# The video model a free account can run (480p or 768p, 4 to 15 seconds).
|
|
22
|
+
MODEL_VIDEO = "minimax-h3-fast"
|
|
23
|
+
|
|
24
|
+
# Model id => the routing field the API expects next to it. Unlisted ids go as "kie".
|
|
25
|
+
MODELS = {
|
|
26
|
+
"saymaker-image-v1" => "kie", "nano-banana-2-lite" => "kie", "nano-banana-2" => "poyo",
|
|
27
|
+
"nano-banana-pro" => "poyo", "gpt-image-2" => "poyo", "gpt-image-2-5-flare" => "kie",
|
|
28
|
+
"seedream-5-lite" => "poyo", "seedream-5-pro" => "kie", "qwen-image-3-pro" => "kie",
|
|
29
|
+
"grok-imagine-image-2-0" => "poyo", "minimax-h3-fast" => "vgenv", "minimax-h3" => "poyo",
|
|
30
|
+
"seedance-2" => "kie", "seedance-2-fast" => "kie", "seedance-2-5" => "kie", "veo-3-1" => "kie",
|
|
31
|
+
"kling-3-0" => "kie", "kling-3-0-turbo" => "kie", "wan-3-0" => "kie", "ltx-2-5-fast" => "replicate"
|
|
32
|
+
}.freeze
|
|
33
|
+
|
|
34
|
+
class Error < StandardError; end
|
|
35
|
+
# No API key, or the key is invalid or deleted.
|
|
36
|
+
class AuthError < Error; end
|
|
37
|
+
# The account is out of credits.
|
|
38
|
+
class QuotaError < Error; end
|
|
39
|
+
# The model needs a paid plan or a credit pack.
|
|
40
|
+
class PlanError < Error; end
|
|
41
|
+
# Another run on this account is still rendering. Free accounts run one at a time.
|
|
42
|
+
class BusyError < Error; end
|
|
43
|
+
# The content filter or the model refused the prompt. Reword it.
|
|
44
|
+
class RejectedError < Error; end
|
|
45
|
+
|
|
46
|
+
module_function
|
|
47
|
+
|
|
48
|
+
# Generate one image, or edit +image_url+, and return its URL.
|
|
49
|
+
def generate(prompt, image_url: nil, model: nil, aspect_ratio: nil, api_key: nil,
|
|
50
|
+
timeout: 4500, poll_every: 5, base_url: BASE, transport: nil)
|
|
51
|
+
check_url(image_url)
|
|
52
|
+
options = { "size" => aspect_ratio || (image_url ? "auto" : "1:1"), "resolution" => "1K", "n" => 1 }
|
|
53
|
+
options["image_input"] = [image_url] if image_url
|
|
54
|
+
run("image", image_url ? "image-to-image" : "text-to-image",
|
|
55
|
+
model || (image_url ? MODEL_EDIT : MODEL_IMAGE), prompt, options,
|
|
56
|
+
api_key, timeout, poll_every, base_url, transport)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Generate one clip, or animate +image_url+ as the first frame, and return its URL.
|
|
60
|
+
def generate_video(prompt, image_url: nil, model: MODEL_VIDEO, duration: nil, resolution: nil,
|
|
61
|
+
aspect_ratio: "16:9", sound: true, api_key: nil, timeout: 5400,
|
|
62
|
+
poll_every: 10, base_url: BASE, transport: nil)
|
|
63
|
+
check_url(image_url)
|
|
64
|
+
options = { "aspect_ratio" => aspect_ratio, "sound" => sound }
|
|
65
|
+
options["image_input"] = [image_url] if image_url
|
|
66
|
+
options["duration"] = duration if duration
|
|
67
|
+
options["resolution"] = resolution if resolution
|
|
68
|
+
run("video", image_url ? "image-to-video" : "text-to-video", model, prompt, options,
|
|
69
|
+
api_key, timeout, poll_every, base_url, transport)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def check_url(url)
|
|
73
|
+
return if url.nil? || url.start_with?("http://", "https://")
|
|
74
|
+
|
|
75
|
+
raise ArgumentError, "image_url must be a public http(s) url"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def http(url, body, key)
|
|
79
|
+
uri = URI(url)
|
|
80
|
+
req = Net::HTTP::Post.new(uri)
|
|
81
|
+
req["Content-Type"] = "application/json"
|
|
82
|
+
req["Authorization"] = "Bearer #{key}"
|
|
83
|
+
req["User-Agent"] = "saymaker-ruby/#{VERSION}"
|
|
84
|
+
req.body = JSON.generate(body)
|
|
85
|
+
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", read_timeout: 60) { |h| h.request(req) }
|
|
86
|
+
JSON.parse(res.body.to_s.empty? ? "{}" : res.body)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Refusals answer HTTP 200 with code -1; the status code alone reads as success.
|
|
90
|
+
def classify(env)
|
|
91
|
+
msg = (env["message"] || "request refused").to_s
|
|
92
|
+
low = msg.downcase
|
|
93
|
+
return BusyError.new(msg) if (env["data"].is_a?(Hash) && env["data"]["busy"]) || low.include?("is rendering") || low.include?("at once")
|
|
94
|
+
return AuthError.new(msg) if low.include?("api key") || low.include?("no auth")
|
|
95
|
+
return QuotaError.new(msg) if low.include?("insufficient credits")
|
|
96
|
+
return PlanError.new(msg) if low.include?("subscription") || low.include?("plan") || low.include?("sign in")
|
|
97
|
+
|
|
98
|
+
Error.new(msg)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def run(media, scene, model, prompt, options, api_key, timeout, poll_every, base_url, transport)
|
|
102
|
+
raise ArgumentError, "prompt is required" if prompt.to_s.strip.empty?
|
|
103
|
+
|
|
104
|
+
key = api_key || ENV["SAYMAKER_API_KEY"]
|
|
105
|
+
raise AuthError, "pass api_key: or set SAYMAKER_API_KEY; create a key at #{KEYS_URL}" if key.nil? || key.empty?
|
|
106
|
+
|
|
107
|
+
send = transport || method(:http)
|
|
108
|
+
env = send.call("#{base_url}/api/ai/generate", {
|
|
109
|
+
"provider" => MODELS.fetch(model, "kie"), "mediaType" => media, "model" => model,
|
|
110
|
+
"scene" => scene, "prompt" => prompt, "options" => options
|
|
111
|
+
}, key)
|
|
112
|
+
raise classify(env) unless env["code"] == 0
|
|
113
|
+
|
|
114
|
+
data = env["data"] || {}
|
|
115
|
+
# A signed-out request is also a 200 with code 0 — only `wall` tells it apart.
|
|
116
|
+
raise AuthError, "SayMaker needs an API key; create one at #{KEYS_URL}" if data["wall"]
|
|
117
|
+
|
|
118
|
+
task = data["id"] or raise Error, "the service returned no task id"
|
|
119
|
+
# Free-account runs only reach the model on the poll that crosses the end of
|
|
120
|
+
# the queue wait, so polling is what starts the work.
|
|
121
|
+
deadline = Time.now + timeout
|
|
122
|
+
while Time.now < deadline
|
|
123
|
+
sleep poll_every
|
|
124
|
+
begin
|
|
125
|
+
q = send.call("#{base_url}/api/ai/query", { "taskId" => task }, key)
|
|
126
|
+
rescue StandardError
|
|
127
|
+
next # a dropped poll is not a failed generation
|
|
128
|
+
end
|
|
129
|
+
next unless q["code"] == 0
|
|
130
|
+
|
|
131
|
+
d = q["data"] || {}
|
|
132
|
+
%w[cleanImages images videos].each { |f| return d[f][0] if d[f].is_a?(Array) && !d[f].empty? }
|
|
133
|
+
raise RejectedError, "the run failed; reword the prompt rather than retrying" if %w[failed error].include?(d["status"].to_s.downcase)
|
|
134
|
+
end
|
|
135
|
+
raise Error, "deadline passed after #{timeout}s; the run may still finish in your SayMaker history"
|
|
136
|
+
end
|
|
137
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: saymaker
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- SayMaker
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-10-05 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: 'A zero-dependency client for saymaker.ai: text-to-image, photo edits
|
|
14
|
+
and text- or image-to-video across Veo 3.1, Kling 3.0, Seedance 2.0, MiniMax H3,
|
|
15
|
+
Nano Banana 2, GPT Image 2.5 and Seedream 5.0, on one API key.'
|
|
16
|
+
email:
|
|
17
|
+
executables: []
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- LICENSE
|
|
22
|
+
- README.md
|
|
23
|
+
- lib/saymaker.rb
|
|
24
|
+
- lib/saymaker/version.rb
|
|
25
|
+
homepage: https://saymaker.ai/?utm_source=rubygems&utm_medium=package
|
|
26
|
+
licenses:
|
|
27
|
+
- MIT
|
|
28
|
+
metadata:
|
|
29
|
+
homepage_uri: https://saymaker.ai/?utm_source=rubygems&utm_medium=package
|
|
30
|
+
source_code_uri: https://github.com/hanshs474/saymaker-rb
|
|
31
|
+
documentation_uri: https://saymaker.ai/video?utm_source=rubygems&utm_medium=package
|
|
32
|
+
bug_tracker_uri: https://github.com/hanshs474/saymaker-rb/issues
|
|
33
|
+
rubygems_mfa_required: 'false'
|
|
34
|
+
post_install_message:
|
|
35
|
+
rdoc_options: []
|
|
36
|
+
require_paths:
|
|
37
|
+
- lib
|
|
38
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
39
|
+
requirements:
|
|
40
|
+
- - ">="
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '2.6'
|
|
43
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
requirements: []
|
|
49
|
+
rubygems_version: 3.0.3.1
|
|
50
|
+
signing_key:
|
|
51
|
+
specification_version: 4
|
|
52
|
+
summary: 'Generate images and video from Ruby on SayMaker: Veo 3.1, Kling 3.0, Seedance
|
|
53
|
+
2.0, Nano Banana 2 with one API key'
|
|
54
|
+
test_files: []
|