kavel 0.1.0 → 0.2.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 +4 -4
- data/README.md +14 -0
- data/lib/kavel/version.rb +1 -1
- data/lib/kavel.rb +39 -16
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3414ddd1c59fa21b575190d22e60c64d4c5e2ef6c8a2c843b55b3d15a3b29a24
|
|
4
|
+
data.tar.gz: bfbd5468f1109256684a3bd7580abe178cfd7bbb8a1cd07556a7a59043cd2251
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2432f1bddc83e6102612deee1ecbcfff03546827145490977b3bddf126aab46878333525b455f5b659adbe3040f4e58e64e45a864eb81f8f48287dbaa251deab
|
|
7
|
+
data.tar.gz: 283153dc729a97e5e13a0440bd6ebd3bde8360cf82266c96558a93c12b2a840fd29a4e507aa0576fc8c244bceea8d9d986ec863b47a0edf258941080601dcb42
|
data/README.md
CHANGED
|
@@ -55,6 +55,20 @@ when the picture has to contain correctly spelled text, and
|
|
|
55
55
|
[Seedream 5.0 Pro](https://www.kavel.ai/image/seedream-5-pro?utm_source=rubygems&utm_medium=package) for
|
|
56
56
|
photoreal people. [Pricing](https://www.kavel.ai/pricing?utm_source=rubygems&utm_medium=package) has every tier.
|
|
57
57
|
|
|
58
|
+
## Past the free tier: an API key
|
|
59
|
+
|
|
60
|
+
When the free allowance runs out, the error tells you where to go next. Create a key at
|
|
61
|
+
[kavel.ai/settings/apikeys](https://www.kavel.ai/settings/apikeys?utm_source=rubygems&utm_medium=package) — the same account
|
|
62
|
+
you use on the site — and pass it (or set `KAVEL_API_KEY`):
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
img = Kavel.generate(prompt, api_key: ENV["KAVEL_API_KEY"], model: "gpt-image-2")
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
With a key every call runs on your account, exactly as it would on the site: your
|
|
69
|
+
[credits and plan](https://www.kavel.ai/pricing?utm_source=rubygems&utm_medium=package), no per-IP ceiling, no watermark on a
|
|
70
|
+
paid plan, and any image model your plan includes. `Kavel::AuthError` means the key is wrong; `Kavel::QuotaError` with a key means the account is out of credits.
|
|
71
|
+
|
|
58
72
|
## Prompts that work
|
|
59
73
|
|
|
60
74
|
Name the light, the material and the composition — that moves the result more than adjectives do.
|
data/lib/kavel/version.rb
CHANGED
data/lib/kavel.rb
CHANGED
|
@@ -31,17 +31,23 @@ module Kavel
|
|
|
31
31
|
class QuotaError < Error; end
|
|
32
32
|
# The content filter or model refused the prompt. Reword it.
|
|
33
33
|
class RejectedError < Error; end
|
|
34
|
-
# The request needs a signed-in account
|
|
34
|
+
# The request needs a signed-in account, or a plan that includes this model.
|
|
35
35
|
class SignInError < Error; end
|
|
36
|
+
# The API key is invalid or has been deleted.
|
|
37
|
+
class AuthError < Error; end
|
|
36
38
|
# The deadline passed while the job was queued or running.
|
|
37
39
|
class TimeoutError < Error; end
|
|
38
40
|
|
|
39
41
|
class << self
|
|
40
42
|
# Turn a prompt into a new image. Name the light, the material and the composition.
|
|
43
|
+
#
|
|
44
|
+
# Pass +api_key:+ (a key from https://www.kavel.ai/settings/apikeys, or set
|
|
45
|
+
# KAVEL_API_KEY) to run on your account: your credits, your plan's models and
|
|
46
|
+
# no watermark on a paid plan. +model:+ is honoured only with a key.
|
|
41
47
|
def generate(prompt, aspect_ratio: "1:1", **opts)
|
|
42
48
|
raise ArgumentError, "prompt is required" if prompt.to_s.strip.empty?
|
|
43
49
|
|
|
44
|
-
run({ provider: "kie", mediaType: "image", model: MODEL_GENERATE, scene: "text-to-image",
|
|
50
|
+
run({ provider: "kie", mediaType: "image", model: pick_model(opts, MODEL_GENERATE), scene: "text-to-image",
|
|
45
51
|
prompt: prompt, options: { aspect_ratio: aspect_ratio } }, **opts)
|
|
46
52
|
end
|
|
47
53
|
|
|
@@ -50,7 +56,7 @@ module Kavel
|
|
|
50
56
|
raise ArgumentError, "source_url must be a public http(s) url" unless source_url.to_s =~ %r{\Ahttps?://}
|
|
51
57
|
raise ArgumentError, "instruction is required" if instruction.to_s.strip.empty?
|
|
52
58
|
|
|
53
|
-
run({ provider: "kie", mediaType: "image", model: MODEL_EDIT, scene: "image-to-image",
|
|
59
|
+
run({ provider: "kie", mediaType: "image", model: pick_model(opts, MODEL_EDIT), scene: "image-to-image",
|
|
54
60
|
prompt: instruction, options: { image_input: [source_url] } }, **opts)
|
|
55
61
|
end
|
|
56
62
|
|
|
@@ -64,16 +70,19 @@ module Kavel
|
|
|
64
70
|
def parse_submit(env)
|
|
65
71
|
if env["code"] != 0
|
|
66
72
|
msg = env["message"].to_s
|
|
67
|
-
|
|
73
|
+
low = msg.downcase
|
|
74
|
+
raise AuthError, msg if low.include?("invalid api key")
|
|
75
|
+
raise QuotaError, msg if low.include?("insufficient credits")
|
|
76
|
+
raise SignInError, msg if low.include?("sign in") || low.include?("subscription")
|
|
68
77
|
|
|
69
78
|
raise Error, msg
|
|
70
79
|
end
|
|
71
80
|
data = env["data"] || {}
|
|
72
81
|
if data["wall"] == true
|
|
73
|
-
raise QuotaError, "this machine has used its #{IP_DAILY_CEILING} credits for today" if data["reason"] == "anon_ip_daily"
|
|
82
|
+
raise QuotaError, "this machine has used its #{IP_DAILY_CEILING} free credits for today — pass api_key: from #{BASE_URL}/settings/apikeys to keep going" if data["reason"] == "anon_ip_daily"
|
|
74
83
|
raise SignInError, "video" if data["reason"] == "anon_unmetered_video"
|
|
75
84
|
|
|
76
|
-
raise QuotaError, "free allowance spent"
|
|
85
|
+
raise QuotaError, "free allowance spent — pass api_key: from #{BASE_URL}/settings/apikeys to keep going"
|
|
77
86
|
end
|
|
78
87
|
id = data["id"].to_s
|
|
79
88
|
raise Error, "the service returned no task id" if id.empty?
|
|
@@ -83,19 +92,30 @@ module Kavel
|
|
|
83
92
|
|
|
84
93
|
private
|
|
85
94
|
|
|
86
|
-
def
|
|
95
|
+
def pick_model(opts, free)
|
|
96
|
+
key = opts[:api_key] || ENV["KAVEL_API_KEY"]
|
|
97
|
+
key && !key.empty? && opts[:model] ? opts[:model] : free
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def run(payload, base_url: BASE_URL, poll_every: 5, timeout: 360, transport: nil, api_key: nil, model: nil)
|
|
87
101
|
http = transport || method(:request)
|
|
88
102
|
deadline = Time.now + timeout
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
103
|
+
key = api_key || ENV["KAVEL_API_KEY"]
|
|
104
|
+
key = nil if key && key.empty?
|
|
105
|
+
# With a key the account is the identity; without one, a fresh anonymous id.
|
|
106
|
+
headers = key ? { "authorization" => "Bearer #{key}" } : { "x-anon-id" => anon_id }
|
|
107
|
+
task = parse_submit(http.call(:post, "#{base_url}/api/ai/generate", payload, headers))
|
|
108
|
+
|
|
109
|
+
# Queued runs only reach the model on the poll that crosses the end of the
|
|
110
|
+
# wait, so polling is what starts the work.
|
|
95
111
|
while Time.now < deadline
|
|
96
112
|
sleep poll_every
|
|
97
113
|
begin
|
|
98
|
-
env =
|
|
114
|
+
env = if key
|
|
115
|
+
http.call(:post, "#{base_url}/api/ai/query", { taskId: task }, headers)
|
|
116
|
+
else
|
|
117
|
+
http.call(:get, "#{base_url}/api/ai/anon-query?taskId=#{URI.encode_www_form_component(task)}&provider=kie&mediaType=image", nil, headers)
|
|
118
|
+
end
|
|
99
119
|
rescue StandardError => e
|
|
100
120
|
raise if e.is_a?(Error)
|
|
101
121
|
|
|
@@ -104,6 +124,9 @@ module Kavel
|
|
|
104
124
|
next unless env["code"] == 0
|
|
105
125
|
|
|
106
126
|
data = env["data"] || {}
|
|
127
|
+
clean = data["cleanImages"] || []
|
|
128
|
+
return Image.new(clean[0], false) unless clean.empty?
|
|
129
|
+
|
|
107
130
|
images = data["images"] || []
|
|
108
131
|
return Image.new(images[0], (data["watermarked"] || [])[0] == true) unless images.empty?
|
|
109
132
|
raise RejectedError, "prompt refused" if %w[failed error].include?(data["status"])
|
|
@@ -111,10 +134,10 @@ module Kavel
|
|
|
111
134
|
raise TimeoutError, "deadline passed"
|
|
112
135
|
end
|
|
113
136
|
|
|
114
|
-
def request(verb, url, body,
|
|
137
|
+
def request(verb, url, body, headers)
|
|
115
138
|
uri = URI(url)
|
|
116
139
|
req = verb == :post ? Net::HTTP::Post.new(uri) : Net::HTTP::Get.new(uri)
|
|
117
|
-
req[
|
|
140
|
+
headers.each { |k, v| req[k] = v }
|
|
118
141
|
req["user-agent"] = "kavel-rb/#{VERSION}"
|
|
119
142
|
if body
|
|
120
143
|
req["content-type"] = "application/json"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kavel
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kavel
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-14 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: 'A zero-dependency client for the free tier of kavel.ai: text-to-image
|
|
14
14
|
and image editing without an API key, an account or a card.'
|