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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +90 -0
- data/lib/emaki/version.rb +3 -0
- data/lib/emaki.rb +184 -0
- 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.
|
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: []
|