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 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.
@@ -0,0 +1,3 @@
1
+ module SayMaker
2
+ VERSION = "0.1.0"
3
+ end
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: []