kavel 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 +80 -0
- data/lib/kavel/version.rb +3 -0
- data/lib/kavel.rb +127 -0
- metadata +52 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 679fb519c2cc395d4219b8e350479387aed76177523c661eb6f9a4b37c8bc5dd
|
|
4
|
+
data.tar.gz: 4b77c92e7c6e7cc66ffa0432cec5a71b7a9cc1e22c416e04328031bb8cfa1284
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f112956669e912aa67ce9a0ddd20be03e042f2f82e9330f0dba678c8d7c956600eb7f2239b583ba7fa4e297937d2276c56f3f1135d48cb3a6be704a00fddef4d
|
|
7
|
+
data.tar.gz: 599a92a96c155b8bbcd976b76d05e23ceee3b33c0ed99db13d57bd30529302d0350f3fa6d3bb64061e1cea8495f5abed0981d6be8e53fe794064e6d9f78ef74b
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kavel (https://www.kavel.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,80 @@
|
|
|
1
|
+
# kavel
|
|
2
|
+
|
|
3
|
+
Generate **and edit** images from Ruby with no API key, no account and no card.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
gem install kavel
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
require "kavel"
|
|
11
|
+
|
|
12
|
+
img = Kavel.generate(
|
|
13
|
+
"matte black ceramic mug on pale oak, soft window light from the left, shallow depth of field",
|
|
14
|
+
aspect_ratio: "16:9"
|
|
15
|
+
)
|
|
16
|
+
img.url # => "https://cdn.kavel.ai/uploads/kie/image/....webp"
|
|
17
|
+
img.watermarked # => true on the free tier
|
|
18
|
+
|
|
19
|
+
edited = Kavel.edit(img.url, "place the mug on a white marble surface")
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Every other image gem wants a key from OpenAI, fal or Replicate before it runs once. This one talks to
|
|
23
|
+
the free tier of [Kavel](https://www.kavel.ai/?utm_source=rubygems&utm_medium=package) — a client id the
|
|
24
|
+
gem invents rather than an account you register — so a Rake task, a Sidekiq job or a one-off script works
|
|
25
|
+
on a machine with nothing configured.
|
|
26
|
+
|
|
27
|
+
## Why it is shaped this way
|
|
28
|
+
|
|
29
|
+
- **Standard library only.** `net/http` and `json`; nothing to conflict with your Gemfile.
|
|
30
|
+
- **An edit lane.** `Kavel.edit` takes a photo you already have and one sentence describing the change.
|
|
31
|
+
- **Exceptions you can rescue by intent.** `Kavel::QuotaError` means wait or sign in,
|
|
32
|
+
`Kavel::RejectedError` means reword, `Kavel::SignInError` means the request is off the free shelf.
|
|
33
|
+
- **It keeps polling through a dropped connection.** A free run waits in a queue and only reaches the
|
|
34
|
+
model at the end of it; giving up on one failed poll would throw away a job about to run. A call holds
|
|
35
|
+
its thread for a minute or more, so run it from a background job, not a request cycle.
|
|
36
|
+
|
|
37
|
+
## Limits, measured against the running service
|
|
38
|
+
|
|
39
|
+
- The free grant is **15 credits**. An image costs **5**, an edit costs **15**. The package mints a new
|
|
40
|
+
client id per call, so a loop is not capped at three.
|
|
41
|
+
- **30 credits per IP per day** sits on top — about six images from one machine — and surfaces as
|
|
42
|
+
`Kavel::QuotaError`.
|
|
43
|
+
- Free output is **1K and watermarked**; `img.watermarked` tells you instead of leaving you to guess.
|
|
44
|
+
- Free runs queue **25–80 seconds** before the model is called, which is why the default deadline is
|
|
45
|
+
six minutes.
|
|
46
|
+
- **Video does not run anonymously** — the cheapest clip costs more than the grant. The
|
|
47
|
+
[AI video generator](https://www.kavel.ai/video?utm_source=rubygems&utm_medium=package) has a browser
|
|
48
|
+
lane for that.
|
|
49
|
+
|
|
50
|
+
Signing in removes the watermark and opens the full shelf:
|
|
51
|
+
[Nano Banana 2](https://www.kavel.ai/image/nano-banana-2?utm_source=rubygems&utm_medium=package) for up to
|
|
52
|
+
14 reference photos, [GPT Image 2](https://www.kavel.ai/image/gpt-image-2?utm_source=rubygems&utm_medium=package)
|
|
53
|
+
for 4K output, [Qwen Image 3](https://www.kavel.ai/image/qwen-image-3?utm_source=rubygems&utm_medium=package)
|
|
54
|
+
when the picture has to contain correctly spelled text, and
|
|
55
|
+
[Seedream 5.0 Pro](https://www.kavel.ai/image/seedream-5-pro?utm_source=rubygems&utm_medium=package) for
|
|
56
|
+
photoreal people. [Pricing](https://www.kavel.ai/pricing?utm_source=rubygems&utm_medium=package) has every tier.
|
|
57
|
+
|
|
58
|
+
## Prompts that work
|
|
59
|
+
|
|
60
|
+
Name the light, the material and the composition — that moves the result more than adjectives do.
|
|
61
|
+
For edits, say what changes and the model keeps the rest; it is the same
|
|
62
|
+
[Nano Banana 2 Lite](https://www.kavel.ai/image/nano-banana-2-lite?utm_source=rubygems&utm_medium=package)
|
|
63
|
+
lane behind the [AI hairstyle changer](https://www.kavel.ai/image/ai-hairstyle-changer?utm_source=rubygems&utm_medium=package)
|
|
64
|
+
and the [AI outfit generator](https://www.kavel.ai/image/ai-outfit-generator?utm_source=rubygems&utm_medium=package).
|
|
65
|
+
The [free AI image generator](https://www.kavel.ai/image?utm_source=rubygems&utm_medium=package) runs the
|
|
66
|
+
same engines in a browser, and the [showcase](https://www.kavel.ai/showcases?utm_source=rubygems&utm_medium=package)
|
|
67
|
+
has finished output.
|
|
68
|
+
|
|
69
|
+
## Tests
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
ruby test/test_kavel.rb
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Offline: argument validation, client id uniqueness, the quota wall (an HTTP 200 recognised only by a
|
|
76
|
+
field), the sign-in refusal, and polling through a dropped connection.
|
|
77
|
+
|
|
78
|
+
More languages and recipes: [kavel.readthedocs.io](https://kavel.readthedocs.io/).
|
|
79
|
+
|
|
80
|
+
MIT
|
data/lib/kavel.rb
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "securerandom"
|
|
6
|
+
require "uri"
|
|
7
|
+
require_relative "kavel/version"
|
|
8
|
+
|
|
9
|
+
# Generate and edit images with no API key and no account.
|
|
10
|
+
#
|
|
11
|
+
# Calls the free tier of https://www.kavel.ai, which meters its allowance
|
|
12
|
+
# against a client id this library invents rather than an account you register.
|
|
13
|
+
# Measured against the running service: 15 credits per id, 5 per image, 15 per
|
|
14
|
+
# edit, 30 per IP per day. Free output is 1K and watermarked.
|
|
15
|
+
module Kavel
|
|
16
|
+
BASE_URL = "https://www.kavel.ai"
|
|
17
|
+
ANON_GRANT = 15
|
|
18
|
+
COST_TEXT_TO_IMAGE = 5
|
|
19
|
+
COST_EDIT = 15
|
|
20
|
+
IP_DAILY_CEILING = 30
|
|
21
|
+
MODEL_GENERATE = "kavel-image-v1"
|
|
22
|
+
MODEL_EDIT = "nano-banana-2-lite"
|
|
23
|
+
|
|
24
|
+
# One finished picture. +url+ is a permanent CDN link; +watermarked+ says
|
|
25
|
+
# whether a mark was actually drawn.
|
|
26
|
+
Image = Struct.new(:url, :watermarked)
|
|
27
|
+
|
|
28
|
+
# Base error. Rescue the subclass that matches what you want to do next.
|
|
29
|
+
class Error < StandardError; end
|
|
30
|
+
# The free allowance is spent for this id or this machine today. Wait, or sign in.
|
|
31
|
+
class QuotaError < Error; end
|
|
32
|
+
# The content filter or model refused the prompt. Reword it.
|
|
33
|
+
class RejectedError < Error; end
|
|
34
|
+
# The request needs a signed-in account (video, or a model off the free shelf).
|
|
35
|
+
class SignInError < Error; end
|
|
36
|
+
# The deadline passed while the job was queued or running.
|
|
37
|
+
class TimeoutError < Error; end
|
|
38
|
+
|
|
39
|
+
class << self
|
|
40
|
+
# Turn a prompt into a new image. Name the light, the material and the composition.
|
|
41
|
+
def generate(prompt, aspect_ratio: "1:1", **opts)
|
|
42
|
+
raise ArgumentError, "prompt is required" if prompt.to_s.strip.empty?
|
|
43
|
+
|
|
44
|
+
run({ provider: "kie", mediaType: "image", model: MODEL_GENERATE, scene: "text-to-image",
|
|
45
|
+
prompt: prompt, options: { aspect_ratio: aspect_ratio } }, **opts)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Rewrite an existing image. +source_url+ must be a public http(s) url.
|
|
49
|
+
def edit(source_url, instruction, **opts)
|
|
50
|
+
raise ArgumentError, "source_url must be a public http(s) url" unless source_url.to_s =~ %r{\Ahttps?://}
|
|
51
|
+
raise ArgumentError, "instruction is required" if instruction.to_s.strip.empty?
|
|
52
|
+
|
|
53
|
+
run({ provider: "kie", mediaType: "image", model: MODEL_EDIT, scene: "image-to-image",
|
|
54
|
+
prompt: instruction, options: { image_input: [source_url] } }, **opts)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# A fresh client id. A reused id walls partway through a loop.
|
|
58
|
+
def anon_id
|
|
59
|
+
"rb-#{SecureRandom.hex(8)}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Interpret the submit response. The quota wall answers HTTP 200 with code 0,
|
|
63
|
+
# so it can only be recognised by a field.
|
|
64
|
+
def parse_submit(env)
|
|
65
|
+
if env["code"] != 0
|
|
66
|
+
msg = env["message"].to_s
|
|
67
|
+
raise SignInError, msg if msg.downcase.include?("sign in")
|
|
68
|
+
|
|
69
|
+
raise Error, msg
|
|
70
|
+
end
|
|
71
|
+
data = env["data"] || {}
|
|
72
|
+
if data["wall"] == true
|
|
73
|
+
raise QuotaError, "this machine has used its #{IP_DAILY_CEILING} credits for today" if data["reason"] == "anon_ip_daily"
|
|
74
|
+
raise SignInError, "video" if data["reason"] == "anon_unmetered_video"
|
|
75
|
+
|
|
76
|
+
raise QuotaError, "free allowance spent"
|
|
77
|
+
end
|
|
78
|
+
id = data["id"].to_s
|
|
79
|
+
raise Error, "the service returned no task id" if id.empty?
|
|
80
|
+
|
|
81
|
+
id
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def run(payload, base_url: BASE_URL, poll_every: 5, timeout: 360, transport: nil)
|
|
87
|
+
http = transport || method(:request)
|
|
88
|
+
deadline = Time.now + timeout
|
|
89
|
+
id = anon_id
|
|
90
|
+
task = parse_submit(http.call(:post, "#{base_url}/api/ai/generate", payload, id))
|
|
91
|
+
|
|
92
|
+
# Free runs queue 25-80s and only reach the model on the poll that crosses
|
|
93
|
+
# the end of it, so polling is what starts the work.
|
|
94
|
+
query = "#{base_url}/api/ai/anon-query?taskId=#{URI.encode_www_form_component(task)}&provider=kie&mediaType=image"
|
|
95
|
+
while Time.now < deadline
|
|
96
|
+
sleep poll_every
|
|
97
|
+
begin
|
|
98
|
+
env = http.call(:get, query, nil, id)
|
|
99
|
+
rescue StandardError => e
|
|
100
|
+
raise if e.is_a?(Error)
|
|
101
|
+
|
|
102
|
+
next # a dropped poll is not a failed generation
|
|
103
|
+
end
|
|
104
|
+
next unless env["code"] == 0
|
|
105
|
+
|
|
106
|
+
data = env["data"] || {}
|
|
107
|
+
images = data["images"] || []
|
|
108
|
+
return Image.new(images[0], (data["watermarked"] || [])[0] == true) unless images.empty?
|
|
109
|
+
raise RejectedError, "prompt refused" if %w[failed error].include?(data["status"])
|
|
110
|
+
end
|
|
111
|
+
raise TimeoutError, "deadline passed"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def request(verb, url, body, anon_id)
|
|
115
|
+
uri = URI(url)
|
|
116
|
+
req = verb == :post ? Net::HTTP::Post.new(uri) : Net::HTTP::Get.new(uri)
|
|
117
|
+
req["x-anon-id"] = anon_id
|
|
118
|
+
req["user-agent"] = "kavel-rb/#{VERSION}"
|
|
119
|
+
if body
|
|
120
|
+
req["content-type"] = "application/json"
|
|
121
|
+
req.body = JSON.generate(body)
|
|
122
|
+
end
|
|
123
|
+
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", read_timeout: 60) { |h| h.request(req) }
|
|
124
|
+
JSON.parse(res.body)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: kavel
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Kavel
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-09-13 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: 'A zero-dependency client for the free tier of kavel.ai: text-to-image
|
|
14
|
+
and image editing without an API key, an account or a card.'
|
|
15
|
+
email:
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- LICENSE
|
|
21
|
+
- README.md
|
|
22
|
+
- lib/kavel.rb
|
|
23
|
+
- lib/kavel/version.rb
|
|
24
|
+
homepage: https://www.kavel.ai
|
|
25
|
+
licenses:
|
|
26
|
+
- MIT
|
|
27
|
+
metadata:
|
|
28
|
+
homepage_uri: https://www.kavel.ai
|
|
29
|
+
source_code_uri: https://github.com/hanshs474/kavel-rb
|
|
30
|
+
documentation_uri: https://kavel.readthedocs.io/
|
|
31
|
+
bug_tracker_uri: https://github.com/hanshs474/kavel-rb/issues
|
|
32
|
+
rubygems_mfa_required: 'false'
|
|
33
|
+
post_install_message:
|
|
34
|
+
rdoc_options: []
|
|
35
|
+
require_paths:
|
|
36
|
+
- lib
|
|
37
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
38
|
+
requirements:
|
|
39
|
+
- - ">="
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '2.6'
|
|
42
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
requirements: []
|
|
48
|
+
rubygems_version: 3.0.3.1
|
|
49
|
+
signing_key:
|
|
50
|
+
specification_version: 4
|
|
51
|
+
summary: Generate and edit images from Ruby with no API key and no account
|
|
52
|
+
test_files: []
|