emaki 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.
Files changed (6) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +90 -0
  4. data/lib/emaki/version.rb +3 -0
  5. data/lib/emaki.rb +184 -0
  6. metadata +55 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 60391feae004e1d2657423237a25638c2b13f9083fdb654aaee9dc7346951aaa
4
+ data.tar.gz: f60f3d361c2ce90a94a3f166d8052d5d1fbd890d4e07ef8317623a3e2adc9873
5
+ SHA512:
6
+ metadata.gz: 6759f21e11ef03fd0a390ad8c96811389b9bbaec6af5b7b42adbc9e6e318c45cb071272d4fcbec82967179786a5fc54f13b740c8fad13d21aa11fa2363a0836c
7
+ data.tar.gz: db2ee405e755c9acc1fa8a51f44bc23f74d26f8592f228f66b8fb5874692c1f66c70a84bd4cb11d1d53414179ca963a0091f3e6acf668db0c2415f4beef481f0
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 emaki (https://emaki.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,90 @@
1
+ # emaki
2
+
3
+ Official SDK for emaki (emaki.ai), the Japanese-language AI video generator for Seedance 2.5 and Seedance 2.0. Generate video and images from your own code with one API key: [Seedance 2.5](https://emaki.ai/seedance-2-5), [Seedance 2.0](https://emaki.ai/seedance-2), [image to video](https://emaki.ai/image-to-video), plus Nano Banana 2, Seedream and GPT Image for stills. To sharpen a finished clip, use the [AI video upscaler](https://emaki.ai/video-upscaler) on the site.
4
+
5
+ 日本語での使い方は [シーダンス](https://emaki.ai/) へ。
6
+
7
+ ```ruby
8
+ require "emaki"
9
+
10
+ clip = Emaki.generate_video("a paper lantern drifting over a night river, slow dolly")
11
+ img = Emaki.generate_image("a ramen shop at dusk, isometric")
12
+ ```
13
+
14
+ Each call submits the run, waits for it and returns the URL of the finished file.
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ gem install emaki
20
+ ```
21
+
22
+ No dependencies beyond the standard library (`net/http`, `json`).
23
+
24
+ ## API key
25
+
26
+ Sign in on [emaki.ai](https://emaki.ai/?utm_source=rubygems&utm_medium=package), create a key at [emaki.ai/settings/apikeys](https://emaki.ai/settings/apikeys), then export it:
27
+
28
+ ```bash
29
+ export EMAKI_API_KEY=sk-...
30
+ ```
31
+
32
+ A key runs on your account exactly like the site does: same models, same review, and every run costs credits on your emaki account, the same price as on the website.
33
+
34
+ ## Video
35
+
36
+ The default video model is `seedance_2_5` (Seedance 2.5).
37
+
38
+ ```ruby
39
+ clip = Emaki.generate_video("a fox trotting through fresh snow at dawn, tracking shot",
40
+ model: "seedance_2_5", duration: 8, resolution: "720p", aspect_ratio: "9:16")
41
+
42
+ # image to video: the image becomes the first frame
43
+ clip = Emaki.generate_video("the camera slowly orbits the statue",
44
+ model: "seedance_2", image: "https://example.com/statue.jpg")
45
+ ```
46
+
47
+ | model | parameters |
48
+ |---|---|
49
+ | `seedance_2_5` | `duration` 4-30 · `resolution` 480p / 720p / 1080p · `aspect_ratio` 16:9, 4:3, 1:1, 3:4, 9:16, 21:9, adaptive · `generate_audio` · `image` (first frame) · `last_frame_image` |
50
+ | `seedance_2` | `duration` 4-15 · `resolution` 480p / 720p / 1080p · `aspect_ratio` · `generate_audio` · `image` |
51
+ | `seedance_2_fast`, `seedance_2_mini` | `duration` 4-15 · `resolution` 480p / 720p · `aspect_ratio` · `generate_audio` · `image` |
52
+ | `minimax_h3` | `duration` 5-15 · `aspect_ratio` · `image` |
53
+ | `veo_3_1` | `resolution` 720p / 1080p / 4k · `aspectRatio` 16:9 / 9:16 · `image` |
54
+
55
+ Parameters you leave out take the same defaults as the site.
56
+
57
+ ## Images
58
+
59
+ The default image model is `nano_banana_2_lite`, the cheapest one.
60
+
61
+ ```ruby
62
+ url = Emaki.generate_image("isometric diorama of a tea house",
63
+ model: "nano_banana_2", aspect_ratio: "16:9", resolution: "2K")
64
+ ```
65
+
66
+ Image models: `nano_banana_2_lite`, `nano_banana_2` (`resolution` 1K / 2K / 4K), `seedream_5_lite` (`quality` basic / high / ultra), `gpt_image_2` (`resolution` 1K / 2K / 4K); all take `aspect_ratio`.
67
+
68
+ ## Lower level
69
+
70
+ ```ruby
71
+ client = Emaki::Client.new # reads EMAKI_API_KEY
72
+ id = client.submit("a koi pond in the rain", model: "seedance_2_mini")
73
+ st = client.status(id) # st["status"]: "generating" | "completed" | "failed"
74
+ left = client.credits # remaining credits on the account
75
+ ```
76
+
77
+ ## Errors worth catching
78
+
79
+ - `Emaki::AuthError`: no key, or the key is wrong or deleted
80
+ - `Emaki::QuotaError`: the account is out of credits
81
+ - `Emaki::PlanError`: that model unlocks after a credit purchase
82
+ - `Emaki::BusyError`: a generation on this account is already in progress
83
+ - `Emaki::RejectedError`: the content filter refused the prompt or image; reword it rather than retrying
84
+ - `Emaki::ValidationError`: a parameter the API did not accept (the message says which)
85
+ - `Emaki::FailedError`: the run ended as failed; `#failure_code` says why
86
+ - `Emaki::TimeoutError`: the deadline passed (15 minutes by default); the run may still finish in your library
87
+
88
+ Every finished run also lands in your library on emaki.ai.
89
+
90
+ MIT licensed. Made by [emaki](https://emaki.ai/?utm_source=rubygems&utm_medium=package), the Japanese-language home for Seedance.
@@ -0,0 +1,3 @@
1
+ module Emaki
2
+ VERSION = "0.1.0"
3
+ end
data/lib/emaki.rb ADDED
@@ -0,0 +1,184 @@
1
+ # Official SDK for emaki (https://emaki.ai), the Japanese-language AI video
2
+ # generator for Seedance 2.5 and Seedance 2.0.
3
+ #
4
+ # require "emaki"
5
+ # clip = Emaki.generate_video("a paper lantern drifting over a night river")
6
+ # img = Emaki.generate_image("a ramen shop at dusk, isometric")
7
+ #
8
+ # Set EMAKI_API_KEY (create a key at https://emaki.ai/settings/apikeys).
9
+ require "json"
10
+ require "net/http"
11
+ require "uri"
12
+ require_relative "emaki/version"
13
+
14
+ module Emaki
15
+ BASE_URL = "https://emaki.ai".freeze
16
+ KEYS_URL = "https://emaki.ai/settings/apikeys".freeze
17
+ MODEL_VIDEO = "seedance_2_5".freeze
18
+ MODEL_IMAGE = "nano_banana_2_lite".freeze
19
+
20
+ # Base of every error this gem raises. #status is the HTTP status when there was one.
21
+ class Error < StandardError
22
+ attr_reader :status
23
+ def initialize(msg = nil, status: nil)
24
+ super(msg)
25
+ @status = status
26
+ end
27
+ end
28
+ # No API key, or the key is invalid or deleted (HTTP 401).
29
+ class AuthError < Error; end
30
+ # The account is out of credits (HTTP 402).
31
+ class QuotaError < Error; end
32
+ # The model unlocks after a credit purchase (HTTP 403).
33
+ class PlanError < Error; end
34
+ # A generation on this account is already in progress (HTTP 409).
35
+ class BusyError < Error; end
36
+ # The content filter refused the prompt or image; reword it rather than retrying.
37
+ class RejectedError < Error; end
38
+ # A parameter the API did not accept (HTTP 400).
39
+ class ValidationError < Error; end
40
+ # The run ended as failed. #failure_code says why.
41
+ class FailedError < Error
42
+ attr_reader :failure_code
43
+ def initialize(code)
44
+ @failure_code = code
45
+ super("emaki: generation failed (#{code})")
46
+ end
47
+ end
48
+ # The deadline passed; the run may still finish in your emaki library.
49
+ class TimeoutError < Error; end
50
+
51
+ # Map an HTTP status and error body to the error that says what to do next.
52
+ def self.classify(status, body)
53
+ body = {} unless body.is_a?(Hash)
54
+ msg = (body["error"] || body["message"] || "request failed").to_s
55
+ tag = body["code"].is_a?(String) ? body["code"] : ""
56
+ klass =
57
+ case
58
+ when status == 401 then AuthError
59
+ when status == 402 then QuotaError
60
+ when status == 403 then PlanError
61
+ when status == 409 then BusyError
62
+ when %w[content_cooldown content_blocked].include?(tag) then RejectedError
63
+ when status == 400 then ValidationError
64
+ # The credits endpoint answers HTTP 200 with code -1.
65
+ when status == 200 && (msg.downcase.include?("api key") || msg.downcase.include?("unauthorized")) then AuthError
66
+ else Error
67
+ end
68
+ klass.new(klass == Error ? "HTTP #{status}: #{msg}" : msg, status: status)
69
+ end
70
+
71
+ # A client bound to one API key. transport: is a callable (url, body, key) -> [status, hash], for tests.
72
+ class Client
73
+ attr_accessor :poll_every, :timeout
74
+
75
+ def initialize(api_key = nil, base_url: BASE_URL, poll_every: 5, timeout: 900, transport: nil)
76
+ @key = (api_key || ENV["EMAKI_API_KEY"]).to_s.strip
77
+ @base = base_url.chomp("/")
78
+ @poll_every = poll_every
79
+ @timeout = timeout
80
+ @transport = transport || method(:http)
81
+ end
82
+
83
+ # Start a run and return its id without waiting.
84
+ def submit(prompt, model: MODEL_VIDEO, **params)
85
+ raise ValidationError, "prompt is required" if prompt.to_s.strip.empty?
86
+
87
+ status, body = call("/api/video/ai", { "prompt" => prompt, "model" => model, "parameters" => stringify(params) })
88
+ id = body.is_a?(Hash) && body["success"] == true && body["data"].is_a?(Hash) && body["data"]["id"]
89
+ return id if status == 200 && id
90
+
91
+ raise Emaki.classify(status, body)
92
+ end
93
+
94
+ # Look up a run; returns the API's data hash ("id", "status", "videoUrl", "failureCode", ...).
95
+ def status(id)
96
+ status, body = call("/api/video/status", { "videoId" => id })
97
+ return body["data"] if status == 200 && body.is_a?(Hash) && body["success"] == true && body["data"].is_a?(Hash)
98
+
99
+ raise Emaki.classify(status, body)
100
+ end
101
+
102
+ # Credits left on the account. Free to call.
103
+ def credits
104
+ status, body = call("/api/user/get-user-credits", {})
105
+ if status == 200 && body.is_a?(Hash) && body["code"] == 0 && body["data"].is_a?(Hash)
106
+ return body["data"]["remainingCredits"].to_i
107
+ end
108
+
109
+ raise Emaki.classify(status, body)
110
+ end
111
+
112
+ # Submit, wait, and return the URL of the finished file.
113
+ def generate(prompt, model:, **params)
114
+ wait(submit(prompt, model: model, **params))
115
+ end
116
+
117
+ # Wait for a submitted run and return its URL.
118
+ def wait(id)
119
+ deadline = Time.now + @timeout
120
+ while Time.now < deadline
121
+ sleep @poll_every
122
+ begin
123
+ d = status(id)
124
+ rescue AuthError
125
+ raise
126
+ rescue StandardError
127
+ next # a dropped poll is not a failed run
128
+ end
129
+ case d["status"]
130
+ when "completed", "succeeded"
131
+ return d["videoUrl"] if d["videoUrl"]
132
+ when "failed"
133
+ raise FailedError, (d["failureCode"] || "unknown")
134
+ end
135
+ end
136
+ raise TimeoutError, "emaki: deadline passed after #{@timeout}s; the run may still finish in your library"
137
+ end
138
+
139
+ private
140
+
141
+ def call(path, body)
142
+ raise AuthError, "set EMAKI_API_KEY or pass api_key; create a key at #{KEYS_URL}" if @key.empty?
143
+
144
+ @transport.call("#{@base}#{path}", body, @key)
145
+ end
146
+
147
+ def stringify(h)
148
+ h.each_with_object({}) { |(k, v), o| o[k.to_s] = v }
149
+ end
150
+
151
+ def http(url, body, key)
152
+ uri = URI(url)
153
+ req = Net::HTTP::Post.new(uri)
154
+ req["Content-Type"] = "application/json"
155
+ req["Authorization"] = "Bearer #{key}"
156
+ req["User-Agent"] = "emaki-ruby/#{VERSION}"
157
+ req.body = JSON.generate(body)
158
+ res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", read_timeout: 60) { |h| h.request(req) }
159
+ parsed = begin
160
+ JSON.parse(res.body.to_s.empty? ? "{}" : res.body)
161
+ rescue JSON::ParserError
162
+ {}
163
+ end
164
+ [res.code.to_i, parsed]
165
+ rescue SocketError, SystemCallError, Net::OpenTimeout, Net::ReadTimeout, OpenSSL::SSL::SSLError => e
166
+ raise Error, "emaki: #{e.message}"
167
+ end
168
+ end
169
+
170
+ # Generate a clip (Seedance 2.5 by default) and return its URL.
171
+ def self.generate_video(prompt, model: MODEL_VIDEO, api_key: nil, **params)
172
+ Client.new(api_key).generate(prompt, model: model, **params)
173
+ end
174
+
175
+ # Generate an image (the cheapest model by default) and return its URL.
176
+ def self.generate_image(prompt, model: MODEL_IMAGE, api_key: nil, **params)
177
+ Client.new(api_key, poll_every: 3).generate(prompt, model: model, **params)
178
+ end
179
+
180
+ # Credits left on the account.
181
+ def self.credits(api_key: nil)
182
+ Client.new(api_key).credits
183
+ end
184
+ end
metadata ADDED
@@ -0,0 +1,55 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: emaki
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - emaki
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-10-16 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: 'Official SDK for emaki (emaki.ai), the Japanese-language AI video generator
14
+ for Seedance 2.5 and Seedance 2.0. A zero-dependency Ruby client: text-to-video
15
+ and image-to-video with Seedance 2.5, Seedance 2.0, MiniMax H3 and Veo 3.1, and
16
+ images with Nano Banana 2, Seedream 5 and GPT Image 2, on one API key.'
17
+ email:
18
+ executables: []
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - LICENSE
23
+ - README.md
24
+ - lib/emaki.rb
25
+ - lib/emaki/version.rb
26
+ homepage: https://emaki.ai/?utm_source=rubygems&utm_medium=package
27
+ licenses:
28
+ - MIT
29
+ metadata:
30
+ homepage_uri: https://emaki.ai/?utm_source=rubygems&utm_medium=package
31
+ source_code_uri: https://github.com/hanshs474/emaki-rb
32
+ documentation_uri: https://emaki.ai/seedance-2-5
33
+ bug_tracker_uri: https://github.com/hanshs474/emaki-rb/issues
34
+ rubygems_mfa_required: 'false'
35
+ post_install_message:
36
+ rdoc_options: []
37
+ require_paths:
38
+ - lib
39
+ required_ruby_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '2.6'
44
+ required_rubygems_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ requirements: []
50
+ rubygems_version: 3.0.3.1
51
+ signing_key:
52
+ specification_version: 4
53
+ summary: Official SDK for emaki (emaki.ai), the Japanese-language AI video generator
54
+ for Seedance 2.5 and Seedance 2.0
55
+ test_files: []